]> git.ipfire.org Git - people/shoehn/ipfire.org.git/blob - webapp/backend/mirrors.py
fireinfo: Fix layout and disabled module.
[people/shoehn/ipfire.org.git] / webapp / backend / mirrors.py
1 #!/usr/bin/python
2
3 from __future__ import division
4
5 import datetime
6 import logging
7 import math
8 import os.path
9 import random
10 import socket
11 import time
12 import tornado.httpclient
13 import tornado.netutil
14 import urlparse
15
16 from misc import Object
17
18 class Downloads(Object):
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):
27 ret = self.db.get("SELECT COUNT(*) AS today FROM log_download WHERE date::date = NOW()::date")
28
29 return ret.today
30
31 @property
32 def yesterday(self):
33 ret = self.db.get("SELECT COUNT(*) AS yesterday FROM log_download WHERE date::date = (NOW() - INTERVAL '1 day')::date")
34
35 return ret.yesterday
36
37 @property
38 def daily_map(self):
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")
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":
48 query += " WHERE date::date = NOW()::date"
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])
56 if count:
57 for res in results:
58 ret[res.country_code] = res.count / count
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":
66 query += " WHERE date::date = NOW()::date"
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])
74 if count:
75 for res in results:
76 mirror = self.mirrors.get(res.mirror)
77 ret[mirror.hostname] = res.count / count
78
79 return ret
80
81
82 class Mirrors(Object):
83 def check_all(self):
84 for mirror in self.get_all():
85 mirror.check()
86
87 def get(self, id):
88 return Mirror(self.backend, id)
89
90 def get_all(self):
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))
99
100 def get_all_up(self):
101 res = self.db.query("SELECT * FROM mirrors WHERE enabled = %s AND state = %s \
102 ORDER BY hostname", True, "UP")
103
104 mirrors = []
105 for row in res:
106 m = Mirror(self.backend, row.id, row)
107 mirrors.append(m)
108
109 return MirrorSet(self.backend, mirrors)
110
111 def get_by_hostname(self, hostname):
112 mirror = self.db.get("SELECT id FROM mirrors WHERE hostname=%s", hostname)
113
114 return Mirror(self.backend, mirror.id)
115
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
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)
141
142 def get_for_location(self, location):
143 if not location:
144 return None
145
146 distance = 2500
147
148 mirrors = []
149 all_mirrors = self.get_all()
150
151 while all_mirrors and len(mirrors) <= 3 and distance <= 8000:
152 for mirror in all_mirrors:
153 mirror_distance = mirror.distance_to(location)
154 if mirror_distance is None:
155 continue
156
157 if mirror_distance<= distance:
158 mirrors.append(mirror)
159 all_mirrors.remove(mirror)
160
161 distance *= 1.2
162
163 return mirrors
164
165 def get_all_files(self):
166 files = []
167
168 for mirror in self.get_all():
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
178
179 class MirrorSet(Object):
180 def __init__(self, backend, mirrors):
181 Object.__init__(self, backend)
182
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
194 return MirrorSet(self.backend, mirrors)
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
203 return MirrorSet(self.backend, mirrors)
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
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
222 return MirrorSet(self.backend, mirrors)
223
224 def get_random(self):
225 mirrors = []
226 for mirror in self._mirrors:
227 for i in range(0, mirror.priority):
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
239 return MirrorSet(self.backend, mirrors)
240
241 def get_for_location(self, location):
242 distance = 2500
243 mirrors = []
244
245 if location:
246 while len(mirrors) <= 3 and distance <= 8000:
247 for mirror in self._mirrors:
248 if mirror in mirrors:
249 continue
250
251 mirror_distance = mirror.distance_to(location)
252 if mirror_distance is None:
253 continue
254
255 if mirror_distance <= distance:
256 mirrors.append(mirror)
257
258 distance *= 1.2
259
260 return MirrorSet(self.backend, mirrors)
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
269 return MirrorSet(self.backend, mirrors)
270
271
272 class Mirror(Object):
273 def __init__(self, backend, id, data=None):
274 Object.__init__(self, backend)
275
276 self.id = id
277
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()
283
284 self.__location = None
285 self.__country_name = None
286
287 def __repr__(self):
288 return "<%s %s>" % (self.__class__.__name__, self.url)
289
290 def __cmp__(self, other):
291 ret = cmp(self.country_code, other.country_code)
292
293 if not ret:
294 ret = cmp(self.hostname, other.hostname)
295
296 return ret
297
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
307 @property
308 def hostname(self):
309 return self._info.hostname
310
311 @property
312 def path(self):
313 return self._info.path
314
315 @property
316 def address(self):
317 for addr in self.addresses4:
318 return addr
319
320 for addr in self.addresses6:
321 return addr
322
323 @property
324 def owner(self):
325 return self._info.owner
326
327 @property
328 def location(self):
329 if self.__location is None:
330 self.__location = self.geoip.get_location(self.address)
331
332 return self.__location
333
334 @property
335 def latitude(self):
336 if self.location:
337 return self.location.latitude
338
339 @property
340 def longitude(self):
341 if self.location:
342 return self.location.longitude
343
344 @property
345 def coordinates(self):
346 return (self.latitude, self.longitude)
347
348 @property
349 def coordiante_str(self):
350 coordinates = []
351
352 for i in self.coordinates:
353 coordinates.append("%s" % i)
354
355 return ",".join(coordinates)
356
357 @property
358 def country_code(self):
359 if self.location:
360 return self.location.country
361
362 @property
363 def country_name(self):
364 if self.__country_name is None:
365 self.__country_name = self.geoip.get_country_name(self.country_code)
366
367 return self.__country_name
368
369 @property
370 def location_str(self):
371 location = []
372
373 if self._info.location:
374 location.append(self._info.location)
375
376 elif self.location:
377 location.append(self.location.city)
378 location.append(self.country_name)
379
380 return ", ".join([s for s in location if s])
381
382 @property
383 def asn(self):
384 if not hasattr(self, "__asn"):
385 self.__asn = self.geoip.get_asn(self.address)
386
387 return self.__asn
388
389 @property
390 def filelist(self):
391 filelist = self.db.query("SELECT filename FROM mirror_files WHERE mirror=%s ORDER BY filename", self.id)
392 return [f.filename for f in filelist]
393
394 @property
395 def prefix(self):
396 return ""
397
398 @property
399 def url(self):
400 return self._info.url
401
402 def build_url(self, filename):
403 return urlparse.urljoin(self.url, filename)
404
405 @property
406 def last_update(self):
407 return self._info.last_update
408
409 @property
410 def state(self):
411 return self._info.state
412
413 def set_state(self, state):
414 logging.info("Setting state of %s to %s" % (self.hostname, state))
415
416 if self.state == state:
417 return
418
419 self.db.execute("UPDATE mirrors SET state = %s WHERE id = %s", state, self.id)
420
421 # Reload changed settings
422 if hasattr(self, "_info"):
423 self._info["state"] = state
424
425 @property
426 def enabled(self):
427 return self._info.enabled
428
429 @property
430 def disabled(self):
431 return not self.enabled
432
433 def check(self):
434 logging.info("Running check for mirror %s" % self.hostname)
435
436 self.check_timestamp()
437 self.check_filelist()
438
439 def check_state(self):
440 logging.debug("Checking state of mirror %s" % self.id)
441
442 if not self.enabled:
443 self.set_state("DOWN")
444 return
445
446 now = datetime.datetime.utcnow()
447
448 time_delta = now - self.last_update
449 time_diff = time_delta.total_seconds()
450
451 time_down = self.settings.get_int("mirrors_time_down", 3*24*60*60)
452 if time_diff >= time_down:
453 self.set_state("DOWN")
454 return
455
456 time_outofsync = self.settings.get_int("mirrors_time_outofsync", 6*60*60)
457 if time_diff >= time_outofsync:
458 self.set_state("OUTOFSYNC")
459 return
460
461 self.set_state("UP")
462
463 def check_timestamp(self):
464 http = tornado.httpclient.AsyncHTTPClient()
465
466 http.fetch(self.url + ".timestamp",
467 headers={ "Pragma" : "no-cache" },
468 callback=self.__check_timestamp_response)
469
470 def __check_timestamp_response(self, response):
471 if response.error:
472 logging.debug("Error getting timestamp from %s" % self.hostname)
473 self.set_state("DOWN")
474 return
475
476 try:
477 timestamp = int(response.body.strip())
478 except ValueError:
479 timestamp = 0
480
481 timestamp = datetime.datetime.utcfromtimestamp(timestamp)
482
483 self.db.execute("UPDATE mirrors SET last_update = %s WHERE id = %s",
484 timestamp, self.id)
485
486 # Reload changed settings
487 if hasattr(self, "_info"):
488 self._info["timestamp"] = timestamp
489
490 self.check_state()
491
492 logging.info("Successfully updated timestamp from %s" % self.hostname)
493
494 def check_filelist(self):
495 # XXX need to remove data from disabled mirrors
496 if not self.enabled:
497 return
498
499 http = tornado.httpclient.AsyncHTTPClient()
500
501 http.fetch(self.url + ".filelist",
502 headers={ "Pragma" : "no-cache" },
503 callback=self.__check_filelist_response)
504
505 def __check_filelist_response(self, response):
506 if response.error:
507 logging.debug("Error getting timestamp from %s" % self.hostname)
508 return
509
510 files = self.filelist
511
512 for file in response.body.splitlines():
513 file = os.path.join(self.prefix, file)
514
515 if file in files:
516 files.remove(file)
517 continue
518
519 self.db.execute("INSERT INTO mirror_files(mirror, filename) VALUES(%s, %s)",
520 self.id, file)
521
522 for file in files:
523 self.db.execute("DELETE FROM mirror_files WHERE mirror=%s AND filename=%s",
524 self.id, file)
525
526 logging.info("Successfully updated mirror filelist from %s" % self.hostname)
527
528 @property
529 def prefer_for_countries(self):
530 countries = self._info.get("prefer_for_countries", "")
531 if countries:
532 return sorted(countries.split(", "))
533
534 return []
535
536 @property
537 def prefer_for_countries_names(self):
538 countries = [self.geoip.get_country_name(c.upper()) for c in self.prefer_for_countries]
539
540 return sorted(countries)
541
542 def distance_to(self, location, ignore_preference=False):
543 if not location:
544 return None
545
546 country_code = None
547 if location.country:
548 country_code = location.country.lower()
549
550 if not ignore_preference and country_code in self.prefer_for_countries:
551 return 0
552
553 # http://www.movable-type.co.uk/scripts/latlong.html
554
555 if self.latitude is None:
556 return None
557
558 if self.longitude is None:
559 return None
560
561 earth = 6371 # km
562 delta_lat = math.radians(self.latitude - location.latitude)
563 delta_lon = math.radians(self.longitude - location.longitude)
564
565 lat1 = math.radians(self.latitude)
566 lat2 = math.radians(location.latitude)
567
568 a = math.sin(delta_lat / 2) ** 2
569 a += math.cos(lat1) * math.cos(lat2) * (math.sin(delta_lon / 2) ** 2)
570
571 b1 = math.sqrt(a)
572 b2 = math.sqrt(1 - a)
573
574 c = 2 * math.atan2(b1, b2)
575
576 return c * earth
577
578 def traffic(self, since):
579 # XXX needs to be done better
580
581 files = {}
582 for entry in self.db.query("SELECT filename, filesize FROM files"):
583 files[entry.filename] = entry.filesize
584
585 query = "SELECT COUNT(filename) as count, filename FROM log_download WHERE mirror = %s"
586 query += " AND date >= %s GROUP BY filename"
587
588 traffic = 0
589 for entry in self.db.query(query, self.id, since):
590 if files.has_key(entry.filename):
591 traffic += entry.count * files[entry.filename]
592
593 return traffic
594
595 @property
596 def priority(self):
597 return self._info.get("priority", 10)
598
599 @property
600 def development(self):
601 return self._info.get("mirrorlist_devel", False)
602
603 @property
604 def mirrorlist(self):
605 return self._info.get("mirrorlist", False)
606
607 @property
608 def addresses(self):
609 if not hasattr(self, "__addresses"):
610 try:
611 addrinfo = socket.getaddrinfo(self.hostname, 0, socket.AF_UNSPEC, socket.SOCK_STREAM)
612 except:
613 raise Exception("Could not resolve %s" % self.hostname)
614
615 ret = []
616 for family, socktype, proto, canonname, address in addrinfo:
617 if family == socket.AF_INET:
618 address, port = address
619 elif family == socket.AF_INET6:
620 address, port, flowid, scopeid = address
621 ret.append((family, address))
622
623 self.__addresses = ret
624
625 return self.__addresses
626
627 @property
628 def addresses6(self):
629 return [address for family, address in self.addresses if family == socket.AF_INET6]
630
631 @property
632 def addresses4(self):
633 return [address for family, address in self.addresses if family == socket.AF_INET]