]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/mirrors.py
configure: Fail if tools are missing
[ipfire.org.git] / src / backend / mirrors.py
CommitLineData
940227cb
MT
1#!/usr/bin/python
2
9068dba1 3import datetime
940227cb 4import logging
7a9e9176 5import iso3166
0673d1b0 6import math
54af860e 7import os.path
0673d1b0 8import random
940227cb
MT
9import socket
10import time
27bcdc3f 11import ssl
940227cb 12import tornado.httpclient
1bb68b0a 13import tornado.iostream
9068dba1 14import tornado.netutil
11347e46 15import urllib.parse
940227cb 16
f110a9ff 17from . import countries
11347e46 18from .misc import Object
95483f04 19from .decorators import *
60024cc8 20
9068dba1 21class 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 84class 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]