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