OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. |
| 4 |
| 5 import logging |
| 6 import threading |
| 7 import time |
| 8 import traceback |
| 9 |
| 10 from utils import net |
| 11 |
| 12 |
| 13 # RemoteClient will attempt to refresh the authentication headers once they are |
| 14 # this close to the expiration. |
| 15 AUTH_HEADERS_EXPIRATION_SEC = 6*60 |
| 16 |
| 17 |
| 18 # How long to wait for a response from the server. Must not be greater than |
| 19 # AUTH_HEADERS_EXPIRATION_SEC, since otherwise there's a chance auth headers |
| 20 # will expire while we wait for connection. |
| 21 NET_CONNECTION_TIMEOUT_SEC = 5*60 |
| 22 |
| 23 |
| 24 class InitializationError(Exception): |
| 25 """Raised by RemoteClient.initialize on fatal errors.""" |
| 26 def __init__(self, last_error): |
| 27 super(InitializationError, self).__init__('Failed to grab auth headers') |
| 28 self.last_error = last_error |
| 29 |
| 30 |
| 31 class RemoteClient(object): |
| 32 """RemoteClient knows how to make authenticated calls to the backend. |
| 33 |
| 34 It also holds in-memory cache of authentication headers and periodically |
| 35 refreshes them (by calling supplied callback, that usually is implemented in |
| 36 terms of bot_config.get_authentication_headers() function). |
| 37 |
| 38 If the callback is None, skips authentication (this is used during initial |
| 39 stages of the bot bootstrap). |
| 40 """ |
| 41 |
| 42 def __init__(self, server, auth_headers_callback): |
| 43 self._server = server |
| 44 self._auth_headers_callback = auth_headers_callback |
| 45 self._lock = threading.Lock() |
| 46 self._headers = None |
| 47 self._exp_ts = None |
| 48 self._disabled = not auth_headers_callback |
| 49 |
| 50 def initialize(self, quit_bit): |
| 51 """Grabs initial auth headers, retrying on errors a bunch of times. |
| 52 |
| 53 Raises InitializationError if all attempts fail. Aborts attempts and returns |
| 54 if quit_bit is signaled. |
| 55 """ |
| 56 attempts = 30 |
| 57 while not quit_bit.is_set(): |
| 58 try: |
| 59 self._get_headers_or_throw() |
| 60 return |
| 61 except Exception as e: |
| 62 last_error = '%s\n%s' % (e, traceback.format_exc()[-2048:]) |
| 63 logging.exception('Failed to grab initial auth headers') |
| 64 attempts -= 1 |
| 65 if not attempts: |
| 66 raise InitializationError(last_error) |
| 67 time.sleep(2) |
| 68 |
| 69 @property |
| 70 def uses_auth(self): |
| 71 """Returns True if get_authentication_headers() returns some headers. |
| 72 |
| 73 If bot_config.get_authentication_headers() is not implement it will return |
| 74 False. |
| 75 """ |
| 76 return bool(self.get_authentication_headers()) |
| 77 |
| 78 def get_authentication_headers(self): |
| 79 """Returns a dict with the headers, refreshing them if necessary. |
| 80 |
| 81 Will always return a dict (perhaps empty if no auth headers are provided by |
| 82 the callback or it has failed). |
| 83 """ |
| 84 try: |
| 85 return self._get_headers_or_throw() |
| 86 except Exception: |
| 87 logging.exception('Failed to refresh auth headers, using cached ones') |
| 88 return self._headers or {} |
| 89 |
| 90 def _get_headers_or_throw(self): |
| 91 if self._disabled: |
| 92 return {} |
| 93 with self._lock: |
| 94 if (self._exp_ts is None or |
| 95 self._exp_ts - time.time() < AUTH_HEADERS_EXPIRATION_SEC): |
| 96 self._headers, self._exp_ts = self._auth_headers_callback() |
| 97 if self._exp_ts is None: |
| 98 logging.info('Not using auth headers') |
| 99 self._disabled = True |
| 100 self._headers = {} |
| 101 else: |
| 102 logging.info( |
| 103 'Refreshed auth headers, they expire in %d sec', |
| 104 self._exp_ts - time.time()) |
| 105 return self._headers or {} |
| 106 |
| 107 def url_read_json(self, url_path, data=None): |
| 108 """Does POST (if data is not None) or GET request to a JSON endpoint.""" |
| 109 return net.url_read_json( |
| 110 self._server + url_path, |
| 111 data=data, |
| 112 headers=self.get_authentication_headers(), |
| 113 timeout=NET_CONNECTION_TIMEOUT_SEC, |
| 114 follow_redirects=False) |
| 115 |
| 116 def url_retrieve(self, filepath, url_path): |
| 117 """Fetches the file from the given URL path on the server.""" |
| 118 return net.url_retrieve( |
| 119 filepath, |
| 120 self._server + url_path, |
| 121 headers=self.get_authentication_headers(), |
| 122 timeout=NET_CONNECTION_TIMEOUT_SEC) |
OLD | NEW |