1# Licensed to the Apache Software Foundation (ASF) under one or more 2# contributor license agreements. See the NOTICE file distributed with 3# this work for additional information regarding copyright ownership. 4# The ASF licenses this file to You under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with 6# the License. You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16from typing import Union 17from typing import Type 18from typing import Optional 19 20import json 21import os 22import ssl 23import socket 24import copy 25import binascii 26import time 27 28from libcloud.utils.py3 import ET 29 30import libcloud 31 32from libcloud.utils.py3 import httplib 33from libcloud.utils.py3 import urlparse 34from libcloud.utils.py3 import urlencode 35 36from libcloud.utils.misc import lowercase_keys 37from libcloud.utils.retry import Retry 38from libcloud.common.exceptions import exception_from_message 39from libcloud.common.types import LibcloudError, MalformedResponseError 40from libcloud.http import LibcloudConnection, HttpLibResponseProxy 41 42__all__ = [ 43 'RETRY_FAILED_HTTP_REQUESTS', 44 45 'BaseDriver', 46 47 'Connection', 48 'PollingConnection', 49 'ConnectionKey', 50 'ConnectionUserAndKey', 51 'CertificateConnection', 52 53 'Response', 54 'HTTPResponse', 55 'JsonResponse', 56 'XmlResponse', 57 'RawResponse' 58] 59 60# Module level variable indicates if the failed HTTP requests should be retried 61RETRY_FAILED_HTTP_REQUESTS = False 62 63# Set to True to allow double slashes in the URL path. This way 64# morph_action_hook() won't strip potentially double slashes in the URLs. 65# This is to support scenarios such as this one - 66# https://github.com/apache/libcloud/issues/1529. 67# We default it to False for backward compatibility reasons. 68ALLOW_PATH_DOUBLE_SLASHES = False 69 70 71class LazyObject(object): 72 """An object that doesn't get initialized until accessed.""" 73 74 @classmethod 75 def _proxy(cls, *lazy_init_args, **lazy_init_kwargs): 76 class Proxy(cls, object): 77 _lazy_obj = None 78 79 def __init__(self): 80 # Must override the lazy_cls __init__ 81 pass 82 83 def __getattribute__(self, attr): 84 lazy_obj = object.__getattribute__(self, '_get_lazy_obj')() 85 return getattr(lazy_obj, attr) 86 87 def __setattr__(self, attr, value): 88 lazy_obj = object.__getattribute__(self, '_get_lazy_obj')() 89 setattr(lazy_obj, attr, value) 90 91 def _get_lazy_obj(self): 92 lazy_obj = object.__getattribute__(self, '_lazy_obj') 93 if lazy_obj is None: 94 lazy_obj = cls(*lazy_init_args, **lazy_init_kwargs) 95 object.__setattr__(self, '_lazy_obj', lazy_obj) 96 return lazy_obj 97 98 return Proxy() 99 100 @classmethod 101 def lazy(cls, *lazy_init_args, **lazy_init_kwargs): 102 """Create a lazily instantiated instance of the subclass, cls.""" 103 return cls._proxy(*lazy_init_args, **lazy_init_kwargs) 104 105 106class HTTPResponse(httplib.HTTPResponse): 107 # On python 2.6 some calls can hang because HEAD isn't quite properly 108 # supported. 109 # In particular this happens on S3 when calls are made to get_object to 110 # objects that don't exist. 111 # This applies the behaviour from 2.7, fixing the hangs. 112 def read(self, amt=None): 113 if self.fp is None: 114 return '' 115 116 if self._method == 'HEAD': 117 self.close() 118 return '' 119 120 return httplib.HTTPResponse.read(self, amt) 121 122 123class Response(object): 124 """ 125 A base Response class to derive from. 126 """ 127 128 # Response status code 129 status = httplib.OK # type: int 130 # Response headers 131 headers = {} # type: dict 132 133 # Raw response body 134 body = None 135 136 # Parsed response body 137 object = None 138 139 error = None # Reason returned by the server. 140 connection = None # Parent connection class 141 parse_zero_length_body = False 142 143 def __init__(self, response, connection): 144 """ 145 :param response: HTTP response object. (optional) 146 :type response: :class:`httplib.HTTPResponse` 147 148 :param connection: Parent connection object. 149 :type connection: :class:`.Connection` 150 """ 151 self.connection = connection 152 153 # http.client In Python 3 doesn't automatically lowercase the header 154 # names 155 self.headers = lowercase_keys(dict(response.headers)) 156 self.error = response.reason 157 self.status = response.status_code 158 self.request = response.request 159 self.iter_content = response.iter_content 160 161 self.body = response.text.strip() \ 162 if response.text is not None and hasattr(response.text, 'strip') \ 163 else '' 164 165 if not self.success(): 166 raise exception_from_message(code=self.status, 167 message=self.parse_error(), 168 headers=self.headers) 169 170 self.object = self.parse_body() 171 172 def parse_body(self): 173 """ 174 Parse response body. 175 176 Override in a provider's subclass. 177 178 :return: Parsed body. 179 :rtype: ``str`` 180 """ 181 return self.body if self.body is not None else '' 182 183 def parse_error(self): 184 """ 185 Parse the error messages. 186 187 Override in a provider's subclass. 188 189 :return: Parsed error. 190 :rtype: ``str`` 191 """ 192 return self.body 193 194 def success(self): 195 """ 196 Determine if our request was successful. 197 198 The meaning of this can be arbitrary; did we receive OK status? Did 199 the node get created? Were we authenticated? 200 201 :rtype: ``bool`` 202 :return: ``True`` or ``False`` 203 """ 204 # pylint: disable=E1101 205 import requests 206 return self.status in [requests.codes.ok, requests.codes.created, 207 httplib.OK, httplib.CREATED, httplib.ACCEPTED] 208 209 210class JsonResponse(Response): 211 """ 212 A Base JSON Response class to derive from. 213 """ 214 215 def parse_body(self): 216 if len(self.body) == 0 and not self.parse_zero_length_body: 217 return self.body 218 219 try: 220 body = json.loads(self.body) 221 except Exception: 222 raise MalformedResponseError( 223 'Failed to parse JSON', 224 body=self.body, 225 driver=self.connection.driver) 226 return body 227 228 parse_error = parse_body 229 230 231class XmlResponse(Response): 232 """ 233 A Base XML Response class to derive from. 234 """ 235 236 def parse_body(self): 237 if len(self.body) == 0 and not self.parse_zero_length_body: 238 return self.body 239 240 try: 241 try: 242 body = ET.XML(self.body) 243 except ValueError: 244 # lxml wants a bytes and tests are basically hard-coded to str 245 body = ET.XML(self.body.encode('utf-8')) 246 except Exception: 247 raise MalformedResponseError('Failed to parse XML', 248 body=self.body, 249 driver=self.connection.driver) 250 return body 251 parse_error = parse_body 252 253 254class RawResponse(Response): 255 def __init__(self, connection, response=None): 256 """ 257 :param connection: Parent connection object. 258 :type connection: :class:`.Connection` 259 """ 260 self._status = None 261 self._response = None 262 self._headers = {} 263 self._error = None 264 self._reason = None 265 self.connection = connection 266 if response is not None: 267 self.headers = lowercase_keys(dict(response.headers)) 268 self.error = response.reason 269 self.status = response.status_code 270 self.request = response.request 271 self.iter_content = response.iter_content 272 273 def success(self): 274 """ 275 Determine if our request was successful. 276 277 The meaning of this can be arbitrary; did we receive OK status? Did 278 the node get created? Were we authenticated? 279 280 :rtype: ``bool`` 281 :return: ``True`` or ``False`` 282 """ 283 # pylint: disable=E1101 284 import requests 285 return self.status in [requests.codes.ok, requests.codes.created, 286 httplib.OK, httplib.CREATED, httplib.ACCEPTED] 287 288 @property 289 def response(self): 290 if not self._response: 291 response = self.connection.connection.getresponse() 292 self._response = HttpLibResponseProxy(response) 293 if not self.success(): 294 self.parse_error() 295 return self._response 296 297 @property 298 def body(self): 299 # Note: We use property to avoid saving whole response body into RAM 300 # See https://github.com/apache/libcloud/pull/1132 for details 301 return self.response.body 302 303 @property 304 def reason(self): 305 if not self._reason: 306 self._reason = self.response.reason 307 return self._reason 308 309 310class Connection(object): 311 """ 312 A Base Connection class to derive from. 313 """ 314 conn_class = LibcloudConnection 315 316 responseCls = Response 317 rawResponseCls = RawResponse 318 retryCls = Retry 319 connection = None 320 host = '127.0.0.1' # type: str 321 port = 443 322 timeout = None # type: Optional[Union[int, float]] 323 secure = 1 324 driver = None # type: Type[BaseDriver] 325 action = None 326 cache_busting = False 327 backoff = None 328 retry_delay = None 329 330 allow_insecure = True 331 332 def __init__(self, secure=True, host=None, port=None, url=None, 333 timeout=None, proxy_url=None, retry_delay=None, backoff=None): 334 self.secure = secure and 1 or 0 335 self.ua = [] 336 self.context = {} 337 338 if not self.allow_insecure and not secure: 339 # TODO: We should eventually switch to whitelist instead of 340 # blacklist approach 341 raise ValueError('Non https connections are not allowed (use ' 342 'secure=True)') 343 344 self.request_path = '' 345 346 if host: 347 self.host = host 348 349 if port is not None: 350 self.port = port 351 else: 352 if self.secure == 1: 353 self.port = 443 354 else: 355 self.port = 80 356 357 if url: 358 (self.host, self.port, self.secure, 359 self.request_path) = self._tuple_from_url(url) 360 361 self.timeout = timeout or self.timeout 362 self.retry_delay = retry_delay 363 self.backoff = backoff 364 self.proxy_url = proxy_url 365 366 def set_http_proxy(self, proxy_url): 367 """ 368 Set a HTTP / HTTPS proxy which will be used with this connection. 369 370 :param proxy_url: Proxy URL (e.g. http://<hostname>:<port> without 371 authentication and 372 <scheme>://<username>:<password>@<hostname>:<port> 373 for basic auth authentication information. 374 :type proxy_url: ``str`` 375 """ 376 self.proxy_url = proxy_url 377 378 # NOTE: Because of the way connection instantion works, we need to call 379 # self.connection.set_http_proxy() here. Just setting "self.proxy_url" 380 # won't work. 381 self.connection.set_http_proxy(proxy_url=proxy_url) 382 383 def set_context(self, context): 384 if not isinstance(context, dict): 385 raise TypeError('context needs to be a dictionary') 386 387 self.context = context 388 389 def reset_context(self): 390 self.context = {} 391 392 def _tuple_from_url(self, url): 393 secure = 1 394 port = None 395 (scheme, netloc, request_path, param, 396 query, fragment) = urlparse.urlparse(url) 397 398 if scheme not in ['http', 'https']: 399 raise LibcloudError('Invalid scheme: %s in url %s' % (scheme, url)) 400 401 if scheme == "http": 402 secure = 0 403 404 if ":" in netloc: 405 netloc, port = netloc.rsplit(":") 406 port = int(port) 407 408 if not port: 409 if scheme == "http": 410 port = 80 411 else: 412 port = 443 413 414 host = netloc 415 port = int(port) 416 417 return (host, port, secure, request_path) 418 419 def connect(self, host=None, port=None, base_url=None, **kwargs): 420 """ 421 Establish a connection with the API server. 422 423 :type host: ``str`` 424 :param host: Optional host to override our default 425 426 :type port: ``int`` 427 :param port: Optional port to override our default 428 429 :returns: A connection 430 """ 431 # prefer the attribute base_url if its set or sent 432 connection = None 433 secure = self.secure 434 435 if getattr(self, 'base_url', None) and base_url is None: 436 (host, port, 437 secure, request_path) = \ 438 self._tuple_from_url(getattr(self, 'base_url')) 439 elif base_url is not None: 440 (host, port, 441 secure, request_path) = self._tuple_from_url(base_url) 442 else: 443 host = host or self.host 444 port = port or self.port 445 446 # Make sure port is an int 447 port = int(port) 448 449 if not hasattr(kwargs, 'host'): 450 kwargs.update({'host': host}) 451 452 if not hasattr(kwargs, 'port'): 453 kwargs.update({'port': port}) 454 455 if not hasattr(kwargs, 'secure'): 456 kwargs.update({'secure': self.secure}) 457 458 if not hasattr(kwargs, 'key_file') and hasattr(self, 'key_file'): 459 kwargs.update({'key_file': getattr(self, 'key_file')}) 460 461 if not hasattr(kwargs, 'cert_file') and hasattr(self, 'cert_file'): 462 kwargs.update({'cert_file': getattr(self, 'cert_file')}) 463 464 if self.timeout: 465 kwargs.update({'timeout': self.timeout}) 466 467 if self.proxy_url: 468 kwargs.update({'proxy_url': self.proxy_url}) 469 470 connection = self.conn_class(**kwargs) 471 # You can uncoment this line, if you setup a reverse proxy server 472 # which proxies to your endpoint, and lets you easily capture 473 # connections in cleartext when you setup the proxy to do SSL 474 # for you 475 # connection = self.conn_class("127.0.0.1", 8080) 476 477 self.connection = connection 478 479 def _user_agent(self): 480 user_agent_suffix = ' '.join(['(%s)' % x for x in self.ua]) 481 482 if self.driver: 483 user_agent = 'libcloud/%s (%s) %s' % ( 484 libcloud.__version__, 485 self.driver.name, user_agent_suffix) 486 else: 487 user_agent = 'libcloud/%s %s' % ( 488 libcloud.__version__, user_agent_suffix) 489 490 return user_agent 491 492 def user_agent_append(self, token): 493 """ 494 Append a token to a user agent string. 495 496 Users of the library should call this to uniquely identify their 497 requests to a provider. 498 499 :type token: ``str`` 500 :param token: Token to add to the user agent. 501 """ 502 self.ua.append(token) 503 504 def request(self, action, params=None, data=None, headers=None, 505 method='GET', raw=False, stream=False, json=None, 506 retry_failed=None): 507 """ 508 Request a given `action`. 509 510 Basically a wrapper around the connection 511 object's `request` that does some helpful pre-processing. 512 513 :type action: ``str`` 514 :param action: A path. This can include arguments. If included, 515 any extra parameters are appended to the existing ones. 516 517 :type params: ``dict`` 518 :param params: Optional mapping of additional parameters to send. If 519 None, leave as an empty ``dict``. 520 521 :type data: ``unicode`` 522 :param data: A body of data to send with the request. 523 524 :type headers: ``dict`` 525 :param headers: Extra headers to add to the request 526 None, leave as an empty ``dict``. 527 528 :type method: ``str`` 529 :param method: An HTTP method such as "GET" or "POST". 530 531 :type raw: ``bool`` 532 :param raw: True to perform a "raw" request aka only send the headers 533 and use the rawResponseCls class. This is used with 534 storage API when uploading a file. 535 536 :type stream: ``bool`` 537 :param stream: True to return an iterator in Response.iter_content 538 and allow streaming of the response data 539 (for downloading large files) 540 541 :param retry_failed: True if failed requests should be retried. This 542 argument can override module level constant and 543 environment variable value on per-request basis. 544 545 :return: An :class:`Response` instance. 546 :rtype: :class:`Response` instance 547 548 """ 549 if params is None: 550 params = {} 551 else: 552 params = copy.copy(params) 553 554 if headers is None: 555 headers = {} 556 else: 557 headers = copy.copy(headers) 558 559 retry_enabled = os.environ.get('LIBCLOUD_RETRY_FAILED_HTTP_REQUESTS', 560 False) or RETRY_FAILED_HTTP_REQUESTS 561 562 # Method level argument has precedence over module level constant and 563 # environment variable 564 if retry_failed is not None: 565 retry_enabled = retry_failed 566 567 action = self.morph_action_hook(action) 568 self.action = action 569 self.method = method 570 self.data = data 571 572 # Extend default parameters 573 params = self.add_default_params(params) 574 575 # Add cache busting parameters (if enabled) 576 if self.cache_busting and method == 'GET': 577 params = self._add_cache_busting_to_params(params=params) 578 579 # Extend default headers 580 headers = self.add_default_headers(headers) 581 582 # We always send a user-agent header 583 headers.update({'User-Agent': self._user_agent()}) 584 585 # Indicate that we support gzip and deflate compression 586 headers.update({'Accept-Encoding': 'gzip,deflate'}) 587 588 port = int(self.port) 589 590 if port not in (80, 443): 591 headers.update({'Host': "%s:%d" % (self.host, port)}) 592 else: 593 headers.update({'Host': self.host}) 594 595 if data: 596 data = self.encode_data(data) 597 598 params, headers = self.pre_connect_hook(params, headers) 599 600 if params: 601 if '?' in action: 602 url = '&'.join((action, urlencode(params, doseq=True))) 603 else: 604 url = '?'.join((action, urlencode(params, doseq=True))) 605 else: 606 url = action 607 608 # IF connection has not yet been established 609 if self.connection is None: 610 self.connect() 611 612 try: 613 # @TODO: Should we just pass File object as body to request method 614 # instead of dealing with splitting and sending the file ourselves? 615 if raw: 616 self.connection.prepared_request( 617 method=method, 618 url=url, 619 body=data, 620 headers=headers, 621 raw=raw, 622 stream=stream) 623 else: 624 if retry_enabled: 625 retry_request = self.retryCls(retry_delay=self.retry_delay, 626 timeout=self.timeout, 627 backoff=self.backoff) 628 retry_request(self.connection.request)(method=method, 629 url=url, 630 body=data, 631 headers=headers, 632 stream=stream) 633 else: 634 self.connection.request(method=method, url=url, body=data, 635 headers=headers, stream=stream) 636 except socket.gaierror as e: 637 message = str(e) 638 errno = getattr(e, 'errno', None) 639 640 if errno == -5: 641 # Throw a more-friendly exception on "no address associated 642 # with hostname" error. This error could simply indicate that 643 # "host" Connection class attribute is set to an incorrect 644 # value 645 class_name = self.__class__.__name__ 646 msg = ('%s. Perhaps "host" Connection class attribute ' 647 '(%s.connection) is set to an invalid, non-hostname ' 648 'value (%s)?' % 649 (message, class_name, self.host)) 650 raise socket.gaierror(msg) 651 self.reset_context() 652 raise e 653 except ssl.SSLError as e: 654 self.reset_context() 655 raise ssl.SSLError(str(e)) 656 657 if raw: 658 responseCls = self.rawResponseCls 659 kwargs = {'connection': self, 660 'response': self.connection.getresponse()} 661 else: 662 responseCls = self.responseCls 663 kwargs = {'connection': self, 664 'response': self.connection.getresponse()} 665 666 try: 667 response = responseCls(**kwargs) 668 finally: 669 # Always reset the context after the request has completed 670 self.reset_context() 671 672 return response 673 674 def morph_action_hook(self, action): 675 """ 676 Here we strip any duplicated leading or traling slashes to 677 prevent typos and other issues where some APIs don't correctly 678 handle double slashes. 679 680 Keep in mind that in some situations, "/" is a vallid path name 681 so we have a module flag which disables this behavior 682 (https://github.com/apache/libcloud/issues/1529). 683 """ 684 if ALLOW_PATH_DOUBLE_SLASHES: 685 # Special case to support scenarios where double slashes are 686 # valid - e.g. for S3 paths - /bucket//path1/path2.txt 687 return self.request_path + action 688 689 url = urlparse.urljoin(self.request_path.lstrip('/').rstrip('/') + 690 '/', action.lstrip('/')) 691 692 if not url.startswith('/'): 693 return '/' + url 694 else: 695 return url 696 697 def add_default_params(self, params): 698 """ 699 Adds default parameters (such as API key, version, etc.) 700 to the passed `params` 701 702 Should return a dictionary. 703 """ 704 return params 705 706 def add_default_headers(self, headers): 707 """ 708 Adds default headers (such as Authorization, X-Foo-Bar) 709 to the passed `headers` 710 711 Should return a dictionary. 712 """ 713 return headers 714 715 def pre_connect_hook(self, params, headers): 716 """ 717 A hook which is called before connecting to the remote server. 718 This hook can perform a final manipulation on the params, headers and 719 url parameters. 720 721 :type params: ``dict`` 722 :param params: Request parameters. 723 724 :type headers: ``dict`` 725 :param headers: Request headers. 726 """ 727 return params, headers 728 729 def encode_data(self, data): 730 """ 731 Encode body data. 732 733 Override in a provider's subclass. 734 """ 735 return data 736 737 def _add_cache_busting_to_params(self, params): 738 """ 739 Add cache busting parameter to the query parameters of a GET request. 740 741 Parameters are only added if "cache_busting" class attribute is set to 742 True. 743 744 Note: This should only be used with *naughty* providers which use 745 excessive caching of responses. 746 """ 747 cache_busting_value = binascii.hexlify(os.urandom(8)).decode('ascii') 748 749 if isinstance(params, dict): 750 params['cache-busting'] = cache_busting_value 751 else: 752 params.append(('cache-busting', cache_busting_value)) 753 754 return params 755 756 757class PollingConnection(Connection): 758 """ 759 Connection class which can also work with the async APIs. 760 761 After initial requests, this class periodically polls for jobs status and 762 waits until the job has finished. 763 If job doesn't finish in timeout seconds, an Exception thrown. 764 """ 765 poll_interval = 0.5 766 timeout = 200 767 request_method = 'request' 768 769 def async_request(self, action, params=None, data=None, headers=None, 770 method='GET', context=None): 771 """ 772 Perform an 'async' request to the specified path. Keep in mind that 773 this function is *blocking* and 'async' in this case means that the 774 hit URL only returns a job ID which is the periodically polled until 775 the job has completed. 776 777 This function works like this: 778 779 - Perform a request to the specified path. Response should contain a 780 'job_id'. 781 782 - Returned 'job_id' is then used to construct a URL which is used for 783 retrieving job status. Constructed URL is then periodically polled 784 until the response indicates that the job has completed or the 785 timeout of 'self.timeout' seconds has been reached. 786 787 :type action: ``str`` 788 :param action: A path 789 790 :type params: ``dict`` 791 :param params: Optional mapping of additional parameters to send. If 792 None, leave as an empty ``dict``. 793 794 :type data: ``unicode`` 795 :param data: A body of data to send with the request. 796 797 :type headers: ``dict`` 798 :param headers: Extra headers to add to the request 799 None, leave as an empty ``dict``. 800 801 :type method: ``str`` 802 :param method: An HTTP method such as "GET" or "POST". 803 804 :type context: ``dict`` 805 :param context: Context dictionary which is passed to the functions 806 which construct initial and poll URL. 807 808 :return: An :class:`Response` instance. 809 :rtype: :class:`Response` instance 810 """ 811 812 request = getattr(self, self.request_method) 813 kwargs = self.get_request_kwargs(action=action, params=params, 814 data=data, headers=headers, 815 method=method, 816 context=context) 817 response = request(**kwargs) 818 kwargs = self.get_poll_request_kwargs(response=response, 819 context=context, 820 request_kwargs=kwargs) 821 822 end = time.time() + self.timeout 823 completed = False 824 while time.time() < end and not completed: 825 response = request(**kwargs) 826 completed = self.has_completed(response=response) 827 if not completed: 828 time.sleep(self.poll_interval) 829 830 if not completed: 831 raise LibcloudError('Job did not complete in %s seconds' % 832 (self.timeout)) 833 834 return response 835 836 def get_request_kwargs(self, action, params=None, data=None, headers=None, 837 method='GET', context=None): 838 """ 839 Arguments which are passed to the initial request() call inside 840 async_request. 841 """ 842 kwargs = {'action': action, 'params': params, 'data': data, 843 'headers': headers, 'method': method} 844 return kwargs 845 846 def get_poll_request_kwargs(self, response, context, request_kwargs): 847 """ 848 Return keyword arguments which are passed to the request() method when 849 polling for the job status. 850 851 :param response: Response object returned by poll request. 852 :type response: :class:`HTTPResponse` 853 854 :param request_kwargs: Kwargs previously used to initiate the 855 poll request. 856 :type response: ``dict`` 857 858 :return ``dict`` Keyword arguments 859 """ 860 raise NotImplementedError('get_poll_request_kwargs not implemented') 861 862 def has_completed(self, response): 863 """ 864 Return job completion status. 865 866 :param response: Response object returned by poll request. 867 :type response: :class:`HTTPResponse` 868 869 :return ``bool`` True if the job has completed, False otherwise. 870 """ 871 raise NotImplementedError('has_completed not implemented') 872 873 874class ConnectionKey(Connection): 875 """ 876 Base connection class which accepts a single ``key`` argument. 877 """ 878 def __init__(self, key, secure=True, host=None, port=None, url=None, 879 timeout=None, proxy_url=None, backoff=None, retry_delay=None): 880 """ 881 Initialize `user_id` and `key`; set `secure` to an ``int`` based on 882 passed value. 883 """ 884 super(ConnectionKey, self).__init__(secure=secure, host=host, 885 port=port, url=url, 886 timeout=timeout, 887 proxy_url=proxy_url, 888 backoff=backoff, 889 retry_delay=retry_delay) 890 self.key = key 891 892 893class CertificateConnection(Connection): 894 """ 895 Base connection class which accepts a single ``cert_file`` argument. 896 """ 897 def __init__(self, cert_file, secure=True, host=None, port=None, url=None, 898 proxy_url=None, timeout=None, backoff=None, retry_delay=None): 899 """ 900 Initialize `cert_file`; set `secure` to an ``int`` based on 901 passed value. 902 """ 903 super(CertificateConnection, self).__init__(secure=secure, host=host, 904 port=port, url=url, 905 timeout=timeout, 906 backoff=backoff, 907 retry_delay=retry_delay, 908 proxy_url=proxy_url) 909 910 self.cert_file = cert_file 911 912 913class KeyCertificateConnection(CertificateConnection): 914 """ 915 Base connection class which accepts both ``key_file`` and ``cert_file`` 916 argument. 917 """ 918 919 key_file = None 920 921 def __init__(self, key_file, cert_file, secure=True, host=None, port=None, 922 url=None, proxy_url=None, timeout=None, backoff=None, 923 retry_delay=None, ca_cert=None): 924 """ 925 Initialize `cert_file`; set `secure` to an ``int`` based on 926 passed value. 927 """ 928 super(KeyCertificateConnection, self).__init__(cert_file, 929 secure=secure, 930 host=host, 931 port=port, url=url, 932 timeout=timeout, 933 backoff=backoff, 934 retry_delay=retry_delay, 935 proxy_url=proxy_url) 936 937 self.key_file = key_file 938 939 940class ConnectionUserAndKey(ConnectionKey): 941 """ 942 Base connection class which accepts a ``user_id`` and ``key`` argument. 943 """ 944 945 user_id = None # type: int 946 947 def __init__(self, user_id, key, secure=True, host=None, port=None, 948 url=None, timeout=None, proxy_url=None, 949 backoff=None, retry_delay=None): 950 super(ConnectionUserAndKey, self).__init__(key, secure=secure, 951 host=host, port=port, 952 url=url, timeout=timeout, 953 backoff=backoff, 954 retry_delay=retry_delay, 955 proxy_url=proxy_url) 956 self.user_id = user_id 957 958 959class BaseDriver(object): 960 """ 961 Base driver class from which other classes can inherit from. 962 """ 963 964 connectionCls = ConnectionKey # type: Type[Connection] 965 966 def __init__(self, key, secret=None, secure=True, host=None, port=None, 967 api_version=None, region=None, **kwargs): 968 """ 969 :param key: API key or username to be used (required) 970 :type key: ``str`` 971 972 :param secret: Secret password to be used (required) 973 :type secret: ``str`` 974 975 :param secure: Whether to use HTTPS or HTTP. Note: Some providers 976 only support HTTPS, and it is on by default. 977 :type secure: ``bool`` 978 979 :param host: Override hostname used for connections. 980 :type host: ``str`` 981 982 :param port: Override port used for connections. 983 :type port: ``int`` 984 985 :param api_version: Optional API version. Only used by drivers 986 which support multiple API versions. 987 :type api_version: ``str`` 988 989 :param region: Optional driver region. Only used by drivers which 990 support multiple regions. 991 :type region: ``str`` 992 993 :rtype: ``None`` 994 """ 995 996 self.key = key 997 self.secret = secret 998 self.secure = secure 999 self.api_version = api_version 1000 self.region = region 1001 1002 conn_kwargs = self._ex_connection_class_kwargs() 1003 conn_kwargs.update({'timeout': kwargs.pop('timeout', None), 1004 'retry_delay': kwargs.pop('retry_delay', None), 1005 'backoff': kwargs.pop('backoff', None), 1006 'proxy_url': kwargs.pop('proxy_url', None)}) 1007 1008 args = [self.key] 1009 1010 if self.secret is not None: 1011 args.append(self.secret) 1012 1013 args.append(secure) 1014 1015 host = conn_kwargs.pop('host', None) or host 1016 1017 if host is not None: 1018 args.append(host) 1019 1020 if port is not None: 1021 args.append(port) 1022 1023 self.connection = self.connectionCls(*args, **conn_kwargs) 1024 self.connection.driver = self 1025 self.connection.connect() 1026 1027 def _ex_connection_class_kwargs(self): 1028 """ 1029 Return extra connection keyword arguments which are passed to the 1030 Connection class constructor. 1031 """ 1032 return {} 1033