Chromium Code Reviews| 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: | |
|
M-A Ruel
2016/06/03 20:00:49
I wish the kind of exceptions trapped was scoped.
Vadim Sh.
2016/06/03 23:37:55
bot_config.py callback can through whatever it wan
| |
| 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 |