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