]> git.ipfire.org Git - people/shoehn/ipfire.org.git/blob - webapp/backend/mirrors.py
Lots of smaller bug fixes.
[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 return socket.gethostbyname(self.hostname)
318
319 @property
320 def owner(self):
321 return self._info.owner
322
323 @property
324 def location(self):
325 if self.__location is None:
326 self.__location = self.geoip.get_location(self.address)
327
328 return self.__location
329
330 @property
331 def latitude(self):
332 if self.location:
333 return self.location.latitude
334
335 @property
336 def longitude(self):
337 if self.location:
338 return self.location.longitude
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
353 @property
354 def country_code(self):
355 if self.location:
356 return self.location.country
357
358 @property
359 def country_name(self):
360 if self.__country_name is None:
361 self.__country_name = self.geoip.get_country_name(self.country_code)
362
363 return self.__country_name
364
365 @property
366 def location_str(self):
367 location = []
368
369 if self._info.location:
370 location.append(self._info.location)
371
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])
377
378 @property
379 def asn(self):
380 if not hasattr(self, "__asn"):
381 self.__asn = self.geoip.get_asn(self.address)
382
383 return self.__asn
384
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
390 @property
391 def prefix(self):
392 return ""
393
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
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
415 self.db.execute("UPDATE mirrors SET state = %s WHERE id = %s", state, self.id)
416
417 # Reload changed settings
418 if hasattr(self, "_info"):
419 self._info["state"] = state
420
421 @property
422 def enabled(self):
423 return self._info.enabled
424
425 @property
426 def disabled(self):
427 return not self.enabled
428
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
438 if not self.enabled:
439 self.set_state("DOWN")
440 return
441
442 now = datetime.datetime.utcnow()
443
444 time_delta = now - self.last_update
445 time_diff = time_delta.total_seconds()
446
447 time_down = self.settings.get_int("mirrors_time_down", 3*24*60*60)
448 if time_diff >= time_down:
449 self.set_state("DOWN")
450 return
451
452 time_outofsync = self.settings.get_int("mirrors_time_outofsync", 6*60*60)
453 if time_diff >= time_outofsync:
454 self.set_state("OUTOFSYNC")
455 return
456
457 self.set_state("UP")
458
459 def check_timestamp(self):
460 http = tornado.httpclient.AsyncHTTPClient()
461
462 http.fetch(self.url + ".timestamp",
463 headers={ "Pragma" : "no-cache" },
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)
469 self.set_state("DOWN")
470 return
471
472 try:
473 timestamp = int(response.body.strip())
474 except ValueError:
475 timestamp = 0
476
477 timestamp = datetime.datetime.utcfromtimestamp(timestamp)
478
479 self.db.execute("UPDATE mirrors SET last_update = %s WHERE id = %s",
480 timestamp, self.id)
481
482 # Reload changed settings
483 if hasattr(self, "_info"):
484 self._info["timestamp"] = timestamp
485
486 self.check_state()
487
488 logging.info("Successfully updated timestamp from %s" % self.hostname)
489
490 def check_filelist(self):
491 # XXX need to remove data from disabled mirrors
492 if not self.enabled:
493 return
494
495 http = tornado.httpclient.AsyncHTTPClient()
496
497 http.fetch(self.url + ".filelist",
498 headers={ "Pragma" : "no-cache" },
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
506 files = self.filelist
507
508 for file in response.body.splitlines():
509 file = os.path.join(self.prefix, file)
510
511 if file in files:
512 files.remove(file)
513 continue
514
515 self.db.execute("INSERT INTO mirror_files(mirror, filename) VALUES(%s, %s)",
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)
521
522 logging.info("Successfully updated mirror filelist from %s" % self.hostname)
523
524 @property
525 def prefer_for_countries(self):
526 countries = self._info.get("prefer_for_countries", "")
527 if countries:
528 return sorted(countries.split(", "))
529
530 return []
531
532 @property
533 def prefer_for_countries_names(self):
534 countries = [self.geoip.get_country_name(c.upper()) for c in self.prefer_for_countries]
535
536 return sorted(countries)
537
538 def distance_to(self, location, ignore_preference=False):
539 if not location:
540 return None
541
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:
547 return 0
548
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)
566
567 b1 = math.sqrt(a)
568 b2 = math.sqrt(1 - a)
569
570 c = 2 * math.atan2(b1, b2)
571
572 return c * earth
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):
593 return self._info.get("priority", 10)
594
595 @property
596 def development(self):
597 return self._info.get("mirrorlist_devel", False)
598
599 @property
600 def mirrorlist(self):
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]