]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/mirrors.py
blog: Remove unused CSS
[ipfire.org.git] / src / backend / mirrors.py
CommitLineData
940227cb
MT
1#!/usr/bin/python
2
9068dba1
MT
3from __future__ import division
4
5import datetime
940227cb 6import logging
0673d1b0 7import math
54af860e 8import os.path
0673d1b0 9import random
940227cb
MT
10import socket
11import time
12import tornado.httpclient
9068dba1
MT
13import tornado.netutil
14import urlparse
940227cb 15
9068dba1 16from misc import Object
60024cc8 17
9068dba1 18class Downloads(Object):
60024cc8
MT
19 @property
20 def total(self):
21 ret = self.db.get("SELECT COUNT(*) AS total FROM log_download")
22
23 return ret.total
24
25 @property
26 def today(self):
9068dba1 27 ret = self.db.get("SELECT COUNT(*) AS today FROM log_download WHERE date::date = NOW()::date")
60024cc8
MT
28
29 return ret.today
30
31 @property
32 def yesterday(self):
9068dba1 33 ret = self.db.get("SELECT COUNT(*) AS yesterday FROM log_download WHERE date::date = (NOW() - INTERVAL '1 day')::date")
60024cc8
MT
34
35 return ret.yesterday
36
37 @property
38 def daily_map(self):
66862195
MT
39 ret = self.db.query("WITH downloads AS (SELECT * FROM log_download \
40 WHERE DATE(date) BETWEEN (NOW()::date - INTERVAL '30 days') AND DATE(NOW())) \
41 SELECT DATE(date) AS date, COUNT(*) AS count FROM downloads \
42 GROUP BY DATE(date) ORDER BY date")
60024cc8
MT
43
44 return ret
45
46 def get_countries(self, duration="all"):
47 query = "SELECT country_code, count(country_code) AS count FROM log_download"
48
49 if duration == "today":
9068dba1 50 query += " WHERE date::date = NOW()::date"
60024cc8
MT
51
52 query += " GROUP BY country_code ORDER BY count DESC"
53
54 results = self.db.query(query)
66862195 55 ret = []
60024cc8
MT
56
57 count = sum([o.count for o in results])
9068dba1
MT
58 if count:
59 for res in results:
66862195 60 ret.append((res.country_code, res.count / count))
60024cc8
MT
61
62 return ret
63
64 def get_mirror_load(self, duration="all"):
65 query = "SELECT mirror, COUNT(mirror) AS count FROM log_download"
66
67 if duration == "today":
9068dba1 68 query += " WHERE date::date = NOW()::date"
60024cc8
MT
69
70 query += " GROUP BY mirror ORDER BY count DESC"
71
72 results = self.db.query(query)
73 ret = {}
74
75 count = sum([o.count for o in results])
9068dba1
MT
76 if count:
77 for res in results:
78 mirror = self.mirrors.get(res.mirror)
79 ret[mirror.hostname] = res.count / count
60024cc8
MT
80
81 return ret
82
83
9068dba1 84class Mirrors(Object):
940227cb 85 def check_all(self):
9068dba1 86 for mirror in self.get_all():
940227cb
MT
87 mirror.check()
88
89 def get(self, id):
9068dba1 90 return Mirror(self.backend, id)
940227cb 91
0673d1b0 92 def get_all(self):
9068dba1
MT
93 res = self.db.query("SELECT * FROM mirrors WHERE enabled = %s", True)
94
95 mirrors = []
96 for row in res:
97 mirror = Mirror(self.backend, row.id, row)
98 mirrors.append(mirror)
99
100 return MirrorSet(self.backend, sorted(mirrors))
0673d1b0 101
940227cb 102 def get_by_hostname(self, hostname):
5488a9f4 103 ret = self.db.get("SELECT * FROM mirrors WHERE hostname = %s", hostname)
940227cb 104
5488a9f4
MT
105 if ret:
106 return Mirror(self.backend, ret.id, ret)
940227cb 107
54af860e
MT
108 def get_with_file(self, filename, country=None):
109 # XXX quick and dirty solution - needs a performance boost
110 mirror_ids = [m.mirror for m in self.db.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename)]
111
112 #if country:
113 # # Sort out all mirrors that are not preferred to the given country
114 # for mirror in self.get_for_country(country):
115 # if not mirror.id in mirror_ids:
116 # mirror_ids.remove(mirror.id)
117
118 mirrors = []
119 for mirror_id in mirror_ids:
120 mirror = self.get(mirror_id)
121 if not mirror.state == "UP":
122 continue
123 mirrors.append(mirror)
124
54af860e
MT
125 return mirrors
126
5488a9f4 127 def get_for_location(self, location, max_distance=4000, filename=None):
119f55d7 128 if not location:
5488a9f4
MT
129 return []
130
131 if filename:
132 res = self.db.query("\
133 WITH client AS (SELECT point(%s, %s) AS location) \
134 SELECT * FROM mirrors WHERE mirrors.state = %s \
135 AND mirrors.id IN ( \
136 SELECT mirror FROM mirror_files WHERE filename = %s \
137 ) AND mirrors.id IN ( \
138 SELECT id FROM mirrors_locations, client \
139 WHERE geodistance(mirrors_locations.location, client.location) <= %s \
140 )",
141 location.latitude, location.longitude, "UP", filename, max_distance)
142 else:
143 res = self.db.query("\
144 WITH client AS (SELECT point(%s, %s) AS location) \
145 SELECT * FROM mirrors WHERE mirrors.state = %s AND mirrors.id IN ( \
146 SELECT id FROM mirrors_locations, client \
147 WHERE geodistance(mirrors_locations.location, client.location) <= %s \
148 )",
149 location.latitude, location.longitude, "UP", max_distance)
9068dba1
MT
150
151 mirrors = []
5488a9f4
MT
152 for row in res:
153 mirror = Mirror(self.backend, row.id, row)
154 mirrors.append(mirror)
0673d1b0 155
1b048628 156 return sorted(mirrors, reverse=True)
0673d1b0 157
edd297c4
MT
158 def get_all_files(self):
159 files = []
160
9068dba1 161 for mirror in self.get_all():
edd297c4
MT
162 if not mirror.state == "UP":
163 continue
164
165 for file in mirror.filelist:
166 if not file in files:
167 files.append(file)
168
169 return files
170
5488a9f4
MT
171 def get_random(self, filename=None):
172 if filename:
173 ret = self.db.get("SELECT * FROM mirrors WHERE state = %s \
174 AND mirrors.id IN (SELECT mirror FROM mirror_files \
175 WHERE filename = %s) ORDER BY RANDOM() LIMIT 1", "UP", filename)
176 else:
177 ret = self.db.get("SELECT * FROM mirrors WHERE state = %s \
178 ORDER BY RANDOM() LIMIT 1", "UP")
179
180 if ret:
181 return Mirror(self.backend, ret.id, ret)
182
183 def file_exists(self, filename):
184 ret = self.db.get("SELECT 1 FROM mirror_files \
185 WHERE filename = %s LIMIT 1", filename)
186
187 if ret:
188 return True
189
190 return False
191
940227cb 192
9068dba1
MT
193class MirrorSet(Object):
194 def __init__(self, backend, mirrors):
195 Object.__init__(self, backend)
196
0673d1b0
MT
197 self._mirrors = mirrors
198
199 def __add__(self, other):
200 mirrors = []
201
202 for mirror in self._mirrors + other._mirrors:
203 if mirror in mirrors:
204 continue
205
206 mirrors.append(mirror)
207
9068dba1 208 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
209
210 def __sub__(self, other):
211 mirrors = self._mirrors[:]
212
213 for mirror in other._mirrors:
214 if mirror in mirrors:
215 mirrors.remove(mirror)
216
9068dba1 217 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
218
219 def __iter__(self):
220 return iter(self._mirrors)
221
222 def __len__(self):
223 return len(self._mirrors)
224
225 def __str__(self):
226 return "<MirrorSet %s>" % ", ".join([m.hostname for m in self._mirrors])
227
0673d1b0
MT
228 def get_with_file(self, filename):
229 with_file = [m.mirror for m in self.db.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename)]
230
231 mirrors = []
232 for mirror in self._mirrors:
233 if mirror.id in with_file:
234 mirrors.append(mirror)
235
9068dba1 236 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
237
238 def get_random(self):
239 mirrors = []
240 for mirror in self._mirrors:
f1f7eb7e 241 for i in range(0, mirror.priority):
0673d1b0
MT
242 mirrors.append(mirror)
243
244 return random.choice(mirrors)
245
9068dba1
MT
246 def get_for_location(self, location):
247 distance = 2500
0673d1b0
MT
248 mirrors = []
249
119f55d7 250 if location:
9068dba1 251 while len(mirrors) <= 3 and distance <= 8000:
119f55d7
MT
252 for mirror in self._mirrors:
253 if mirror in mirrors:
254 continue
0673d1b0 255
9068dba1
MT
256 mirror_distance = mirror.distance_to(location)
257 if mirror_distance is None:
258 continue
259
260 if mirror_distance <= distance:
119f55d7 261 mirrors.append(mirror)
0673d1b0 262
119f55d7 263 distance *= 1.2
0673d1b0 264
9068dba1 265 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
266
267 def get_with_state(self, state):
268 mirrors = []
269
270 for mirror in self._mirrors:
271 if mirror.state == state:
272 mirrors.append(mirror)
273
9068dba1
MT
274 return MirrorSet(self.backend, mirrors)
275
0673d1b0 276
9068dba1
MT
277class Mirror(Object):
278 def __init__(self, backend, id, data=None):
279 Object.__init__(self, backend)
0673d1b0 280
940227cb
MT
281 self.id = id
282
bd17b7d1
MT
283 if data:
284 self._info = data
285 else:
286 self._info = self.db.get("SELECT * FROM mirrors WHERE id = %s", self.id)
287 self._info["url"] = self.generate_url()
940227cb 288
119f55d7
MT
289 self.__location = None
290 self.__country_name = None
291
54af860e
MT
292 def __repr__(self):
293 return "<%s %s>" % (self.__class__.__name__, self.url)
294
295 def __cmp__(self, other):
9068dba1 296 ret = cmp(self.country_code, other.country_code)
54af860e 297
9068dba1
MT
298 if not ret:
299 ret = cmp(self.hostname, other.hostname)
300
301 return ret
940227cb 302
940227cb 303 def generate_url(self):
10cdef58 304 url = "%s://%s" % ("https" if self.supports_https else "http", self.hostname)
940227cb
MT
305 if not self.path.startswith("/"):
306 url += "/"
307 url += "%s" % self.path
308 if not self.path.endswith("/"):
309 url += "/"
310 return url
311
9068dba1
MT
312 @property
313 def hostname(self):
314 return self._info.hostname
315
316 @property
317 def path(self):
318 return self._info.path
940227cb 319
10cdef58
MT
320 @property
321 def supports_https(self):
322 return self._info.supports_https
323
940227cb
MT
324 @property
325 def address(self):
199b04e7 326 for addr in self.addresses4:
b898caea
MT
327 return addr
328
199b04e7 329 for addr in self.addresses6:
b898caea 330 return addr
940227cb 331
9068dba1
MT
332 @property
333 def owner(self):
334 return self._info.owner
335
0673d1b0
MT
336 @property
337 def location(self):
119f55d7 338 if self.__location is None:
9068dba1 339 self.__location = self.geoip.get_location(self.address)
0673d1b0
MT
340
341 return self.__location
342
343 @property
344 def latitude(self):
9068dba1
MT
345 if self.location:
346 return self.location.latitude
0673d1b0
MT
347
348 @property
349 def longitude(self):
9068dba1
MT
350 if self.location:
351 return self.location.longitude
0673d1b0
MT
352
353 @property
354 def coordinates(self):
355 return (self.latitude, self.longitude)
356
357 @property
358 def coordiante_str(self):
359 coordinates = []
360
361 for i in self.coordinates:
362 coordinates.append("%s" % i)
363
364 return ",".join(coordinates)
365
940227cb
MT
366 @property
367 def country_code(self):
9068dba1
MT
368 if self.location:
369 return self.location.country
940227cb 370
0673d1b0
MT
371 @property
372 def country_name(self):
119f55d7 373 if self.__country_name is None:
9068dba1 374 self.__country_name = self.geoip.get_country_name(self.country_code)
119f55d7
MT
375
376 return self.__country_name
0673d1b0
MT
377
378 @property
9068dba1
MT
379 def location_str(self):
380 location = []
381
382 if self._info.location:
383 location.append(self._info.location)
0673d1b0 384
9068dba1
MT
385 elif self.location:
386 location.append(self.location.city)
387 location.append(self.country_name)
388
389 return ", ".join([s for s in location if s])
0673d1b0
MT
390
391 @property
9068dba1
MT
392 def asn(self):
393 if not hasattr(self, "__asn"):
394 self.__asn = self.geoip.get_asn(self.address)
0673d1b0 395
9068dba1 396 return self.__asn
0673d1b0 397
940227cb
MT
398 @property
399 def filelist(self):
400 filelist = self.db.query("SELECT filename FROM mirror_files WHERE mirror=%s ORDER BY filename", self.id)
401 return [f.filename for f in filelist]
402
54af860e
MT
403 @property
404 def prefix(self):
54af860e
MT
405 return ""
406
9068dba1
MT
407 @property
408 def url(self):
409 return self._info.url
410
411 def build_url(self, filename):
412 return urlparse.urljoin(self.url, filename)
413
414 @property
415 def last_update(self):
416 return self._info.last_update
417
418 @property
419 def state(self):
420 return self._info.state
421
940227cb
MT
422 def set_state(self, state):
423 logging.info("Setting state of %s to %s" % (self.hostname, state))
424
425 if self.state == state:
426 return
427
9068dba1 428 self.db.execute("UPDATE mirrors SET state = %s WHERE id = %s", state, self.id)
940227cb
MT
429
430 # Reload changed settings
bd17b7d1
MT
431 if hasattr(self, "_info"):
432 self._info["state"] = state
940227cb 433
9068dba1
MT
434 @property
435 def enabled(self):
436 return self._info.enabled
437
438 @property
439 def disabled(self):
440 return not self.enabled
441
940227cb
MT
442 def check(self):
443 logging.info("Running check for mirror %s" % self.hostname)
444
3ead0979
MT
445 self.db.execute("UPDATE mirrors SET address = %s WHERE id = %s",
446 self.address, self.id)
447
940227cb
MT
448 self.check_timestamp()
449 self.check_filelist()
450
451 def check_state(self):
452 logging.debug("Checking state of mirror %s" % self.id)
453
9068dba1 454 if not self.enabled:
940227cb 455 self.set_state("DOWN")
9068dba1
MT
456 return
457
458 now = datetime.datetime.utcnow()
459
460 time_delta = now - self.last_update
461 time_diff = time_delta.total_seconds()
940227cb 462
9068dba1
MT
463 time_down = self.settings.get_int("mirrors_time_down", 3*24*60*60)
464 if time_diff >= time_down:
940227cb 465 self.set_state("DOWN")
9068dba1 466 return
940227cb 467
9068dba1
MT
468 time_outofsync = self.settings.get_int("mirrors_time_outofsync", 6*60*60)
469 if time_diff >= time_outofsync:
470 self.set_state("OUTOFSYNC")
940227cb
MT
471 return
472
9068dba1
MT
473 self.set_state("UP")
474
475 def check_timestamp(self):
940227cb
MT
476 http = tornado.httpclient.AsyncHTTPClient()
477
478 http.fetch(self.url + ".timestamp",
54af860e 479 headers={ "Pragma" : "no-cache" },
940227cb
MT
480 callback=self.__check_timestamp_response)
481
482 def __check_timestamp_response(self, response):
483 if response.error:
484 logging.debug("Error getting timestamp from %s" % self.hostname)
a3ee39ce 485 self.set_state("DOWN")
940227cb
MT
486 return
487
488 try:
489 timestamp = int(response.body.strip())
490 except ValueError:
491 timestamp = 0
492
ea324f48 493 timestamp = datetime.datetime.utcfromtimestamp(timestamp)
9068dba1
MT
494
495 self.db.execute("UPDATE mirrors SET last_update = %s WHERE id = %s",
940227cb
MT
496 timestamp, self.id)
497
498 # Reload changed settings
bd17b7d1
MT
499 if hasattr(self, "_info"):
500 self._info["timestamp"] = timestamp
940227cb
MT
501
502 self.check_state()
503
504 logging.info("Successfully updated timestamp from %s" % self.hostname)
505
506 def check_filelist(self):
54af860e 507 # XXX need to remove data from disabled mirrors
9068dba1 508 if not self.enabled:
940227cb
MT
509 return
510
511 http = tornado.httpclient.AsyncHTTPClient()
512
513 http.fetch(self.url + ".filelist",
54af860e 514 headers={ "Pragma" : "no-cache" },
940227cb
MT
515 callback=self.__check_filelist_response)
516
517 def __check_filelist_response(self, response):
518 if response.error:
519 logging.debug("Error getting timestamp from %s" % self.hostname)
520 return
521
56b9c1d8 522 files = self.filelist
940227cb
MT
523
524 for file in response.body.splitlines():
56b9c1d8
MT
525 file = os.path.join(self.prefix, file)
526
527 if file in files:
528 files.remove(file)
529 continue
530
940227cb 531 self.db.execute("INSERT INTO mirror_files(mirror, filename) VALUES(%s, %s)",
56b9c1d8
MT
532 self.id, file)
533
534 for file in files:
535 self.db.execute("DELETE FROM mirror_files WHERE mirror=%s AND filename=%s",
536 self.id, file)
940227cb
MT
537
538 logging.info("Successfully updated mirror filelist from %s" % self.hostname)
539
54af860e
MT
540 @property
541 def prefer_for_countries(self):
0673d1b0
MT
542 countries = self._info.get("prefer_for_countries", "")
543 if countries:
544 return sorted(countries.split(", "))
54af860e 545
0673d1b0
MT
546 return []
547
548 @property
549 def prefer_for_countries_names(self):
9068dba1
MT
550 countries = [self.geoip.get_country_name(c.upper()) for c in self.prefer_for_countries]
551
552 return sorted(countries)
54af860e 553
119f55d7 554 def distance_to(self, location, ignore_preference=False):
0673d1b0 555 if not location:
9068dba1 556 return None
940227cb 557
9068dba1
MT
558 country_code = None
559 if location.country:
560 country_code = location.country.lower()
561
562 if not ignore_preference and country_code in self.prefer_for_countries:
0673d1b0
MT
563 return 0
564
9068dba1
MT
565 # http://www.movable-type.co.uk/scripts/latlong.html
566
567 if self.latitude is None:
568 return None
569
570 if self.longitude is None:
571 return None
572
573 earth = 6371 # km
574 delta_lat = math.radians(self.latitude - location.latitude)
575 delta_lon = math.radians(self.longitude - location.longitude)
576
577 lat1 = math.radians(self.latitude)
578 lat2 = math.radians(location.latitude)
579
580 a = math.sin(delta_lat / 2) ** 2
581 a += math.cos(lat1) * math.cos(lat2) * (math.sin(delta_lon / 2) ** 2)
0673d1b0 582
9068dba1
MT
583 b1 = math.sqrt(a)
584 b2 = math.sqrt(1 - a)
0673d1b0 585
9068dba1
MT
586 c = 2 * math.atan2(b1, b2)
587
588 return c * earth
0673d1b0
MT
589
590 def traffic(self, since):
591 # XXX needs to be done better
592
593 files = {}
594 for entry in self.db.query("SELECT filename, filesize FROM files"):
595 files[entry.filename] = entry.filesize
596
597 query = "SELECT COUNT(filename) as count, filename FROM log_download WHERE mirror = %s"
598 query += " AND date >= %s GROUP BY filename"
599
600 traffic = 0
601 for entry in self.db.query(query, self.id, since):
602 if files.has_key(entry.filename):
603 traffic += entry.count * files[entry.filename]
604
605 return traffic
606
607 @property
608 def priority(self):
f1f7eb7e 609 return self._info.get("priority", 10)
940227cb 610
bd17b7d1
MT
611 @property
612 def development(self):
ea324f48 613 return self._info.get("mirrorlist_devel", False)
bd17b7d1
MT
614
615 @property
616 def mirrorlist(self):
9068dba1
MT
617 return self._info.get("mirrorlist", False)
618
619 @property
620 def addresses(self):
621 if not hasattr(self, "__addresses"):
b898caea
MT
622 try:
623 addrinfo = socket.getaddrinfo(self.hostname, 0, socket.AF_UNSPEC, socket.SOCK_STREAM)
624 except:
625 raise Exception("Could not resolve %s" % self.hostname)
9068dba1
MT
626
627 ret = []
628 for family, socktype, proto, canonname, address in addrinfo:
629 if family == socket.AF_INET:
630 address, port = address
631 elif family == socket.AF_INET6:
632 address, port, flowid, scopeid = address
633 ret.append((family, address))
634
635 self.__addresses = ret
636
637 return self.__addresses
638
639 @property
640 def addresses6(self):
641 return [address for family, address in self.addresses if family == socket.AF_INET6]
642
643 @property
644 def addresses4(self):
645 return [address for family, address in self.addresses if family == socket.AF_INET]