|
1 | 1 | import logging |
| 2 | +from datetime import timedelta |
2 | 3 |
|
3 | 4 | import requests |
4 | 5 | from celery import shared_task |
5 | 6 | from django.contrib.gis.geos import Point |
| 7 | +from django.db import transaction |
| 8 | +from django.utils import timezone |
6 | 9 | from django.utils.translation import gettext as _ |
7 | 10 | from geoip2 import errors |
8 | 11 | from geoip2 import webservice as geoip2_webservice |
@@ -59,84 +62,104 @@ def on_failure(self, exc, task_id, args, kwargs, einfo): |
59 | 62 | base=WHOISCeleryRetryTask, |
60 | 63 | **app_settings.API_TASK_RETRY_OPTIONS, |
61 | 64 | ) |
62 | | -def fetch_whois_details(self, device_pk, initial_ip_address, new_ip_address): |
| 65 | +def fetch_whois_details(self, device_pk, initial_ip_address): |
63 | 66 | """ |
64 | 67 | Fetches the WHOIS details of the given IP address |
65 | 68 | and creates/updates the WHOIS record. |
66 | 69 | """ |
67 | 70 | Device = load_model("config", "Device") |
68 | 71 | WHOISInfo = load_model("config", "WHOISInfo") |
69 | 72 |
|
70 | | - # The task can be triggered for same ip address multiple times |
71 | | - # so we need to return early if WHOIS is already created. |
72 | | - if WHOISInfo.objects.filter(ip_address=new_ip_address).exists(): |
73 | | - return |
74 | | - |
75 | | - device = Device.objects.get(pk=device_pk) |
76 | | - # Host is based on the db that is used to fetch the details. |
77 | | - # As we are using GeoLite2, 'geolite.info' host is used. |
78 | | - # Refer: https://geoip2.readthedocs.io/en/latest/#sync-web-service-example |
79 | | - ip_client = geoip2_webservice.Client( |
80 | | - account_id=app_settings.WHOIS_GEOIP_ACCOUNT, |
81 | | - license_key=app_settings.WHOIS_GEOIP_KEY, |
82 | | - host="geolite.info", |
83 | | - ) |
84 | | - |
85 | | - try: |
86 | | - data = ip_client.city(ip_address=new_ip_address) |
87 | | - |
88 | | - # Catching all possible exceptions raised by the geoip2 client |
89 | | - # and raising them with appropriate messages to be handled by the task |
90 | | - # retry mechanism. |
91 | | - except ( |
92 | | - errors.AddressNotFoundError, |
93 | | - errors.AuthenticationError, |
94 | | - errors.OutOfQueriesError, |
95 | | - errors.PermissionRequiredError, |
96 | | - ) as e: |
97 | | - exc_type = type(e) |
98 | | - message = EXCEPTION_MESSAGES.get(exc_type) |
99 | | - if exc_type is errors.AddressNotFoundError: |
100 | | - message = message.format(ip_address=new_ip_address) |
101 | | - raise exc_type(message) |
102 | | - except requests.RequestException as e: |
103 | | - raise e |
104 | | - |
105 | | - else: |
106 | | - # The attributes are always present in the response, |
107 | | - # but they can be None, so added fallbacks. |
108 | | - address = { |
109 | | - "city": data.city.name or "", |
110 | | - "country": data.country.name or "", |
111 | | - "continent": data.continent.name or "", |
112 | | - "postal": str(data.postal.code or ""), |
113 | | - } |
114 | | - coordinates = Point(data.location.longitude, data.location.latitude, srid=4326) |
115 | | - whois_obj = WHOISInfo( |
116 | | - isp=data.traits.autonomous_system_organization, |
117 | | - asn=data.traits.autonomous_system_number, |
118 | | - timezone=data.location.time_zone, |
119 | | - address=address, |
120 | | - cidr=data.traits.network, |
121 | | - ip_address=new_ip_address, |
122 | | - coordinates=coordinates, |
| 73 | + with transaction.atomic(): |
| 74 | + device = Device.objects.get(pk=device_pk) |
| 75 | + new_ip_address = device.last_ip |
| 76 | + # If there is existing WHOIS older record then it needs to be updated |
| 77 | + whois_obj = WHOISInfo.objects.filter(ip_address=new_ip_address).first() |
| 78 | + if whois_obj and (timezone.now() - whois_obj.modified) < timedelta(days=14): |
| 79 | + return |
| 80 | + |
| 81 | + # Host is based on the db that is used to fetch the details. |
| 82 | + # As we are using GeoLite2, 'geolite.info' host is used. |
| 83 | + # Refer: https://geoip2.readthedocs.io/en/latest/#sync-web-service-example |
| 84 | + ip_client = geoip2_webservice.Client( |
| 85 | + account_id=app_settings.WHOIS_GEOIP_ACCOUNT, |
| 86 | + license_key=app_settings.WHOIS_GEOIP_KEY, |
| 87 | + host="geolite.info", |
123 | 88 | ) |
124 | | - whois_obj.full_clean() |
125 | | - whois_obj.save() |
126 | | - logger.info(f"Successfully fetched WHOIS details for {new_ip_address}.") |
127 | 89 |
|
128 | | - if device._get_organization__config_settings().estimated_location_enabled: |
129 | | - manage_estimated_locations.delay( |
130 | | - device_pk=device_pk, ip_address=new_ip_address |
| 90 | + try: |
| 91 | + data = ip_client.city(ip_address=new_ip_address) |
| 92 | + |
| 93 | + # Catching all possible exceptions raised by the geoip2 client |
| 94 | + # and raising them with appropriate messages to be handled by the task |
| 95 | + # retry mechanism. |
| 96 | + except ( |
| 97 | + errors.AddressNotFoundError, |
| 98 | + errors.AuthenticationError, |
| 99 | + errors.OutOfQueriesError, |
| 100 | + errors.PermissionRequiredError, |
| 101 | + ) as e: |
| 102 | + exc_type = type(e) |
| 103 | + message = EXCEPTION_MESSAGES.get(exc_type) |
| 104 | + if exc_type is errors.AddressNotFoundError: |
| 105 | + message = message.format(ip_address=new_ip_address) |
| 106 | + raise exc_type(message) |
| 107 | + except requests.RequestException as e: |
| 108 | + raise e |
| 109 | + |
| 110 | + else: |
| 111 | + # The attributes are always present in the response, |
| 112 | + # but they can be None, so added fallbacks. |
| 113 | + address = { |
| 114 | + "city": data.city.name or "", |
| 115 | + "country": data.country.name or "", |
| 116 | + "continent": data.continent.name or "", |
| 117 | + "postal": str(data.postal.code or ""), |
| 118 | + } |
| 119 | + coordinates = Point( |
| 120 | + data.location.longitude, data.location.latitude, srid=4326 |
131 | 121 | ) |
132 | | - |
133 | | - # delete WHOIS record for initial IP if no devices are linked to it |
134 | | - if ( |
135 | | - not Device.objects.filter(_is_deactivated=False) |
136 | | - .filter(last_ip=initial_ip_address) |
137 | | - .exists() |
138 | | - ): |
139 | | - delete_whois_record(ip_address=initial_ip_address) |
| 122 | + details = { |
| 123 | + "isp": data.traits.autonomous_system_organization, |
| 124 | + "asn": data.traits.autonomous_system_number, |
| 125 | + "timezone": data.location.time_zone, |
| 126 | + "address": address, |
| 127 | + "coordinates": coordinates, |
| 128 | + "cidr": data.traits.network, |
| 129 | + "ip_address": new_ip_address, |
| 130 | + } |
| 131 | + update_fields = [] |
| 132 | + if whois_obj: |
| 133 | + for attr, value in details.items(): |
| 134 | + if getattr(whois_obj, attr) != value: |
| 135 | + update_fields.append(attr) |
| 136 | + setattr(whois_obj, attr, value) |
| 137 | + if update_fields: |
| 138 | + whois_obj.save(update_fields=update_fields) |
| 139 | + else: |
| 140 | + whois_obj = WHOISInfo(**details) |
| 141 | + whois_obj.full_clean() |
| 142 | + whois_obj.save() |
| 143 | + logger.info(f"Successfully fetched WHOIS details for {new_ip_address}.") |
| 144 | + |
| 145 | + if device._get_organization__config_settings().estimated_location_enabled: |
| 146 | + # the estimated location task should not run if old record is updated |
| 147 | + # and location related fields are not updated |
| 148 | + if update_fields and not any( |
| 149 | + i in update_fields for i in ["address", "coordinates"] |
| 150 | + ): |
| 151 | + return |
| 152 | + manage_estimated_locations.delay( |
| 153 | + device_pk=device_pk, ip_address=new_ip_address |
| 154 | + ) |
| 155 | + |
| 156 | + # delete WHOIS record for initial IP if no devices are linked to it |
| 157 | + if ( |
| 158 | + not Device.objects.filter(_is_deactivated=False) |
| 159 | + .filter(last_ip=initial_ip_address) |
| 160 | + .exists() |
| 161 | + ): |
| 162 | + delete_whois_record(ip_address=initial_ip_address) |
140 | 163 |
|
141 | 164 |
|
142 | 165 | @shared_task |
|
0 commit comments