]>
Commit | Line | Data |
---|---|---|
940227cb MT |
1 | #!/usr/bin/python |
2 | ||
9068dba1 | 3 | import datetime |
940227cb | 4 | import logging |
7a9e9176 | 5 | import iso3166 |
0673d1b0 | 6 | import math |
54af860e | 7 | import os.path |
0673d1b0 | 8 | import random |
940227cb MT |
9 | import socket |
10 | import time | |
27bcdc3f | 11 | import ssl |
940227cb | 12 | import tornado.httpclient |
1bb68b0a | 13 | import tornado.iostream |
9068dba1 | 14 | import tornado.netutil |
11347e46 | 15 | import urllib.parse |
940227cb | 16 | |
f110a9ff | 17 | from . import countries |
11347e46 | 18 | from .misc import Object |
95483f04 | 19 | from .decorators import * |
60024cc8 | 20 | |
9068dba1 | 21 | class Mirrors(Object): |
95483f04 MT |
22 | def _get_mirrors(self, query, *args): |
23 | res = self.db.query(query, *args) | |
24 | ||
25 | for row in res: | |
26 | yield Mirror(self.backend, row.id, data=row) | |
27 | ||
f110a9ff MT |
28 | def _get_mirror(self, query, *args): |
29 | res = self.db.get(query, *args) | |
30 | ||
31 | if res: | |
32 | return Mirror(self.backend, res.id, data=res) | |
33 | ||
95483f04 MT |
34 | def __iter__(self): |
35 | mirrors = self._get_mirrors("SELECT * FROM mirrors \ | |
36 | WHERE enabled IS TRUE ORDER BY hostname") | |
37 | ||
38 | return iter(mirrors) | |
39 | ||
9fdf4fb7 | 40 | async def check_all(self): |
95483f04 | 41 | for mirror in self: |
b2059099 | 42 | with self.db.transaction(): |
9fdf4fb7 | 43 | await mirror.check() |
940227cb | 44 | |
f110a9ff MT |
45 | def get_for_download(self, filename, country_code=None): |
46 | # Try to find a good mirror for this country first | |
47 | if country_code: | |
48 | zone = countries.get_zone(country_code) | |
49 | ||
50 | mirror = self._get_mirror("SELECT mirrors.* FROM mirror_files files \ | |
51 | LEFT JOIN mirrors ON files.mirror = mirrors.id \ | |
52 | WHERE files.filename = %s \ | |
53 | AND mirrors.enabled IS TRUE AND mirrors.state = %s \ | |
54 | AND mirrors.country_code = ANY(%s) \ | |
55 | ORDER BY RANDOM() LIMIT 1", filename, "UP", | |
56 | countries.get_in_zone(zone)) | |
57 | ||
58 | if mirror: | |
59 | return mirror | |
60 | ||
61 | # Get a random mirror that serves the file | |
62 | return self._get_mirror("SELECT mirrors.* FROM mirror_files files \ | |
63 | LEFT JOIN mirrors ON files.mirror = mirrors.id \ | |
64 | WHERE files.filename = %s \ | |
65 | AND mirrors.enabled IS TRUE AND mirrors.state = %s \ | |
66 | ORDER BY RANDOM() LIMIT 1", filename, "UP") | |
5488a9f4 | 67 | |
f110a9ff MT |
68 | def get_by_hostname(self, hostname): |
69 | return self._get_mirror("SELECT * FROM mirrors \ | |
70 | WHERE hostname = %s", hostname) | |
5488a9f4 | 71 | |
7a9e9176 MT |
72 | def get_by_countries(self): |
73 | mirrors = {} | |
74 | ||
75 | for m in self: | |
76 | try: | |
77 | mirrors[m.country].append(m) | |
78 | except KeyError: | |
79 | mirrors[m.country] = [m] | |
80 | ||
81 | return mirrors | |
82 | ||
940227cb | 83 | |
9068dba1 | 84 | class Mirror(Object): |
95483f04 MT |
85 | def init(self, id, data=None): |
86 | self.id = id | |
87 | self.data = data | |
940227cb | 88 | |
95483f04 MT |
89 | def __str__(self): |
90 | return self.hostname | |
119f55d7 | 91 | |
54af860e MT |
92 | def __repr__(self): |
93 | return "<%s %s>" % (self.__class__.__name__, self.url) | |
94 | ||
95483f04 MT |
95 | def __eq__(self, other): |
96 | if isinstance(other, self.__class__): | |
97 | return self.id == other.id | |
9068dba1 | 98 | |
95483f04 MT |
99 | def __lt__(self, other): |
100 | if isinstance(other, self.__class__): | |
101 | return self.hostname < other.hostname | |
940227cb | 102 | |
95483f04 MT |
103 | @lazy_property |
104 | def url(self): | |
10cdef58 | 105 | url = "%s://%s" % ("https" if self.supports_https else "http", self.hostname) |
95483f04 | 106 | |
940227cb MT |
107 | if not self.path.startswith("/"): |
108 | url += "/" | |
95483f04 | 109 | |
940227cb | 110 | url += "%s" % self.path |
95483f04 | 111 | |
940227cb MT |
112 | if not self.path.endswith("/"): |
113 | url += "/" | |
95483f04 | 114 | |
940227cb MT |
115 | return url |
116 | ||
9068dba1 MT |
117 | @property |
118 | def hostname(self): | |
95483f04 | 119 | return self.data.hostname |
9068dba1 MT |
120 | |
121 | @property | |
122 | def path(self): | |
95483f04 | 123 | return self.data.path |
940227cb | 124 | |
a69e87a1 MT |
125 | @property |
126 | def supports_https(self): | |
95483f04 | 127 | return self.data.supports_https |
10cdef58 | 128 | |
940227cb MT |
129 | @property |
130 | def address(self): | |
199b04e7 | 131 | for addr in self.addresses4: |
b898caea MT |
132 | return addr |
133 | ||
199b04e7 | 134 | for addr in self.addresses6: |
b898caea | 135 | return addr |
940227cb | 136 | |
9068dba1 MT |
137 | @property |
138 | def owner(self): | |
95483f04 | 139 | return self.data.owner |
9068dba1 | 140 | |
95483f04 | 141 | @lazy_property |
0673d1b0 | 142 | def location(self): |
95483f04 | 143 | return self.geoip.get_location(self.address) |
0673d1b0 MT |
144 | |
145 | @property | |
146 | def latitude(self): | |
9068dba1 MT |
147 | if self.location: |
148 | return self.location.latitude | |
0673d1b0 MT |
149 | |
150 | @property | |
151 | def longitude(self): | |
9068dba1 MT |
152 | if self.location: |
153 | return self.location.longitude | |
0673d1b0 | 154 | |
7a9e9176 MT |
155 | @lazy_property |
156 | def country(self): | |
157 | return iso3166.countries.get(self.country_code) | |
158 | ||
940227cb MT |
159 | @property |
160 | def country_code(self): | |
f110a9ff | 161 | return self.data.country_code |
940227cb | 162 | |
0673d1b0 MT |
163 | @property |
164 | def country_name(self): | |
95483f04 | 165 | return self.geoip.get_country_name(self.country_code) |
0673d1b0 | 166 | |
f110a9ff MT |
167 | @property |
168 | def zone(self): | |
169 | return countries.get_zone(self.country_name) | |
170 | ||
95483f04 | 171 | @lazy_property |
9068dba1 | 172 | def asn(self): |
95483f04 | 173 | return self.geoip.get_asn(self.address) |
0673d1b0 | 174 | |
940227cb MT |
175 | @property |
176 | def filelist(self): | |
177 | filelist = self.db.query("SELECT filename FROM mirror_files WHERE mirror=%s ORDER BY filename", self.id) | |
178 | return [f.filename for f in filelist] | |
179 | ||
54af860e MT |
180 | @property |
181 | def prefix(self): | |
54af860e MT |
182 | return "" |
183 | ||
9068dba1 | 184 | def build_url(self, filename): |
11347e46 | 185 | return urllib.parse.urljoin(self.url, filename) |
9068dba1 MT |
186 | |
187 | @property | |
188 | def last_update(self): | |
95483f04 | 189 | return self.data.last_update |
9068dba1 MT |
190 | |
191 | @property | |
192 | def state(self): | |
95483f04 | 193 | return self.data.state |
9068dba1 | 194 | |
940227cb | 195 | def set_state(self, state): |
fb51c9c7 | 196 | logging.debug("Setting state of %s to %s" % (self.hostname, state)) |
940227cb MT |
197 | |
198 | if self.state == state: | |
199 | return | |
200 | ||
9068dba1 | 201 | self.db.execute("UPDATE mirrors SET state = %s WHERE id = %s", state, self.id) |
940227cb MT |
202 | |
203 | # Reload changed settings | |
95483f04 | 204 | self.data["state"] = state |
940227cb | 205 | |
9068dba1 MT |
206 | @property |
207 | def enabled(self): | |
95483f04 | 208 | return self.data.enabled |
9068dba1 MT |
209 | |
210 | @property | |
211 | def disabled(self): | |
212 | return not self.enabled | |
213 | ||
9fdf4fb7 | 214 | async def check(self): |
fb51c9c7 | 215 | logging.debug("Running check for mirror %s" % self.hostname) |
940227cb | 216 | |
3ead0979 MT |
217 | self.db.execute("UPDATE mirrors SET address = %s WHERE id = %s", |
218 | self.address, self.id) | |
219 | ||
9fdf4fb7 | 220 | success = await self.check_timestamp() |
b2059099 | 221 | if success: |
9fdf4fb7 | 222 | await self.check_filelist() |
b2059099 MT |
223 | |
224 | def check_state(self, last_update): | |
940227cb MT |
225 | logging.debug("Checking state of mirror %s" % self.id) |
226 | ||
9068dba1 | 227 | if not self.enabled: |
940227cb | 228 | self.set_state("DOWN") |
9068dba1 MT |
229 | return |
230 | ||
231 | now = datetime.datetime.utcnow() | |
232 | ||
b2059099 | 233 | time_delta = now - last_update |
9068dba1 | 234 | time_diff = time_delta.total_seconds() |
940227cb | 235 | |
9068dba1 MT |
236 | time_down = self.settings.get_int("mirrors_time_down", 3*24*60*60) |
237 | if time_diff >= time_down: | |
940227cb | 238 | self.set_state("DOWN") |
9068dba1 | 239 | return |
940227cb | 240 | |
9068dba1 MT |
241 | time_outofsync = self.settings.get_int("mirrors_time_outofsync", 6*60*60) |
242 | if time_diff >= time_outofsync: | |
243 | self.set_state("OUTOFSYNC") | |
940227cb MT |
244 | return |
245 | ||
9068dba1 MT |
246 | self.set_state("UP") |
247 | ||
9fdf4fb7 | 248 | async def check_timestamp(self): |
940227cb MT |
249 | http = tornado.httpclient.AsyncHTTPClient() |
250 | ||
b2059099 | 251 | try: |
9fdf4fb7 | 252 | response = await http.fetch(self.url + ".timestamp", |
b2059099 MT |
253 | headers={ "Pragma" : "no-cache" }) |
254 | except tornado.httpclient.HTTPError as e: | |
0661e16b | 255 | logging.warning("Error getting timestamp from %s: %s" % (self.hostname, e)) |
b2059099 MT |
256 | self.set_state("DOWN") |
257 | return False | |
940227cb | 258 | |
27bcdc3f | 259 | except ssl.SSLError as e: |
0661e16b | 260 | logging.warning("SSL error when getting timestamp from %s: %s" % (self.hostname, e)) |
27bcdc3f MT |
261 | self.set_state("DOWN") |
262 | return False | |
263 | ||
1bb68b0a | 264 | except tornado.iostream.StreamClosedError as e: |
0661e16b | 265 | logging.warning("Connection closed unexpectedly for %s: %s" % (self.hostname, e)) |
1bb68b0a MT |
266 | self.set_state("DOWN") |
267 | return False | |
268 | ||
269 | except OSError as e: | |
0661e16b | 270 | logging.warning("Could not connect to %s: %s" % (self.hostname, e)) |
1bb68b0a MT |
271 | self.set_state("DOWN") |
272 | return False | |
273 | ||
940227cb | 274 | if response.error: |
0661e16b | 275 | logging.warning("Error getting timestamp from %s" % self.hostname) |
a3ee39ce | 276 | self.set_state("DOWN") |
940227cb MT |
277 | return |
278 | ||
279 | try: | |
280 | timestamp = int(response.body.strip()) | |
281 | except ValueError: | |
282 | timestamp = 0 | |
283 | ||
ea324f48 | 284 | timestamp = datetime.datetime.utcfromtimestamp(timestamp) |
9068dba1 MT |
285 | |
286 | self.db.execute("UPDATE mirrors SET last_update = %s WHERE id = %s", | |
940227cb MT |
287 | timestamp, self.id) |
288 | ||
b2059099 MT |
289 | # Update state |
290 | self.check_state(timestamp) | |
940227cb | 291 | |
fb51c9c7 MT |
292 | logging.debug("Successfully updated timestamp from %s" % self.hostname) |
293 | ||
b2059099 | 294 | return True |
940227cb | 295 | |
9fdf4fb7 | 296 | async def check_filelist(self): |
54af860e | 297 | # XXX need to remove data from disabled mirrors |
9068dba1 | 298 | if not self.enabled: |
940227cb MT |
299 | return |
300 | ||
301 | http = tornado.httpclient.AsyncHTTPClient() | |
302 | ||
b2059099 | 303 | try: |
9fdf4fb7 | 304 | response = await http.fetch(self.url + ".filelist", |
b2059099 MT |
305 | headers={ "Pragma" : "no-cache" }) |
306 | except tornado.httpclient.HTTPError as e: | |
5c704007 | 307 | logging.warning("Error getting filelist from %s: %s" % (self.hostname, e)) |
b2059099 MT |
308 | self.set_state("DOWN") |
309 | return | |
940227cb | 310 | |
940227cb | 311 | if response.error: |
b2059099 | 312 | logging.debug("Error getting filelist from %s" % self.hostname) |
940227cb MT |
313 | return |
314 | ||
b2059099 MT |
315 | # Drop the old filelist |
316 | self.db.execute("DELETE FROM mirror_files WHERE mirror = %s", self.id) | |
940227cb | 317 | |
b2059099 | 318 | # Add them all again |
940227cb | 319 | for file in response.body.splitlines(): |
b2059099 | 320 | file = os.path.join(self.prefix, file.decode()) |
56b9c1d8 | 321 | |
b2059099 MT |
322 | self.db.execute("INSERT INTO mirror_files(mirror, filename) \ |
323 | VALUES(%s, %s)", self.id, file) | |
940227cb | 324 | |
fb51c9c7 | 325 | logging.debug("Successfully updated mirror filelist from %s" % self.hostname) |
940227cb | 326 | |
bd17b7d1 MT |
327 | @property |
328 | def development(self): | |
95483f04 | 329 | return self.data.get("mirrorlist_devel", False) |
bd17b7d1 MT |
330 | |
331 | @property | |
332 | def mirrorlist(self): | |
95483f04 | 333 | return self.data.get("mirrorlist", False) |
9068dba1 | 334 | |
f110a9ff | 335 | @lazy_property |
9068dba1 | 336 | def addresses(self): |
b064747a | 337 | addrinfo = socket.getaddrinfo(self.hostname, 0, socket.AF_UNSPEC, socket.SOCK_STREAM) |
f110a9ff MT |
338 | |
339 | ret = [] | |
340 | for family, socktype, proto, canonname, address in addrinfo: | |
341 | if family == socket.AF_INET: | |
342 | address, port = address | |
343 | elif family == socket.AF_INET6: | |
344 | address, port, flowid, scopeid = address | |
345 | ret.append((family, address)) | |
346 | ||
347 | return ret | |
9068dba1 MT |
348 | |
349 | @property | |
350 | def addresses6(self): | |
351 | return [address for family, address in self.addresses if family == socket.AF_INET6] | |
352 | ||
353 | @property | |
354 | def addresses4(self): | |
355 | return [address for family, address in self.addresses if family == socket.AF_INET] |