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 |