Skip to content

Commit 69540a8

Browse files
committed
[changes] Tasks Refactoring, documentation update
Signed-off-by: DragnEmperor <[email protected]>
1 parent a2412c5 commit 69540a8

File tree

8 files changed

+272
-238
lines changed

8 files changed

+272
-238
lines changed

docs/user/estimated-location.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ Estimated Location
1616
Overview
1717
--------
1818

19-
The Estimated Location feature automatically creates or updates a device’s
20-
location based on latitude and longitude information retrieved from the
21-
WHOIS Lookup feature.
19+
This feature automatically creates or updates a device’s location based on
20+
latitude and longitude information retrieved from the WHOIS Lookup
21+
feature.
2222

2323
Trigger Conditions
2424
------------------
2525

26-
Estimated Location is triggered when:
26+
This feature is triggered when:
2727

2828
- A **fresh WHOIS lookup** is performed for a device.
2929
- Or when a WHOIS record already exists for the device’s IP **and**:

docs/user/whois.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ WHOIS Lookup
1515
Overview
1616
--------
1717

18-
The WHOIS Lookup feature displays information about the public IP address
19-
used by devices to communicate with OpenWISP (via the ``last_ip`` field).
20-
It helps identify the geographic location and ISP associated with the IP
21-
address, which can be useful for troubleshooting network issues.
18+
This feature displays information about the public IP address used by
19+
devices to communicate with OpenWISP (via the ``last_ip`` field). It helps
20+
identify the geographic location and ISP associated with the IP address,
21+
which can be useful for troubleshooting network issues.
2222

2323
The retrieved information pertains to the Autonomous System (ASN)
2424
associated with the device's public IP address and includes:
@@ -126,6 +126,5 @@ If a record is older than :ref:`Threshold
126126
<openwisp_controller_whois_refresh_threshold_days>`, it will be refreshed
127127
automatically.
128128

129-
This will be triggered for the same scenarios defined in `trigger
130-
conditions <whois_trigger_conditions_>`_ but among the conditions **only
131-
WHOIS enablement is required**.
129+
The update mechanism will be triggered whenever a device is registered or
130+
its last IP changes or fetches its checksum.

openwisp_controller/config/management/commands/clear_last_ip.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55

66
class Command(BaseCommand):
7-
help = "Clear the last IP address, if set, of active devices of all organizations."
7+
help = (
8+
"Clears the last IP address (if set) for every active device"
9+
" across all organizations."
10+
)
811

912
def add_arguments(self, parser):
1013
parser.add_argument(
@@ -29,11 +32,11 @@ def handle(self, *args, **options):
2932
"Are you sure you want to do this?\n\n"
3033
"Type 'yes' to continue, or 'no' to cancel: "
3134
)
32-
if input("".join(message)) != "yes":
35+
if input("".join(message)).lower() != "yes":
3336
raise CommandError("Operation cancelled by user.")
3437

3538
devices = Device.objects.filter(_is_deactivated=False).only("last_ip")
36-
# Filter devices that have no WHOIS information for their last IP
39+
# Filter out devices that have WHOIS information for their last IP
3740
devices = devices.exclude(last_ip=None).exclude(
3841
last_ip__in=Subquery(
3942
WHOISInfo.objects.filter(ip_address=OuterRef("last_ip")).values(

openwisp_controller/config/whois/service.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
from datetime import timedelta
22
from ipaddress import ip_address as ip_addr
33

4+
import requests
5+
from django.contrib.gis.geos import Point
46
from django.core.cache import cache
57
from django.db import transaction
68
from django.utils import timezone
9+
from django.utils.translation import gettext as _
10+
from geoip2 import errors
11+
from geoip2 import webservice as geoip2_webservice
712
from swapper import load_model
813

914
from openwisp_controller.config import settings as app_settings
1015

1116
from .tasks import fetch_whois_details, manage_estimated_locations
1217

18+
EXCEPTION_MESSAGES = {
19+
errors.AddressNotFoundError: _(
20+
"No WHOIS information found for IP address {ip_address}"
21+
),
22+
errors.AuthenticationError: _(
23+
"Authentication failed for GeoIP2 service. "
24+
"Check your OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT and "
25+
"OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY settings."
26+
),
27+
errors.OutOfQueriesError: _(
28+
"Your account has run out of queries for the GeoIP2 service."
29+
),
30+
errors.PermissionRequiredError: _(
31+
"Your account does not have permission to access this service."
32+
),
33+
}
34+
1335

1436
class WHOISService:
1537
"""
@@ -19,6 +41,20 @@ class WHOISService:
1941
def __init__(self, device):
2042
self.device = device
2143

44+
@staticmethod
45+
def geoip_client():
46+
"""
47+
Used to get a GeoIP2 web service client instance.
48+
Host is based on the db that is used to fetch the details.
49+
As we are using GeoLite2, 'geolite.info' host is used.
50+
Refer: https://geoip2.readthedocs.io/en/latest/#sync-web-service-example
51+
"""
52+
return geoip2_webservice.Client(
53+
account_id=app_settings.WHOIS_GEOIP_ACCOUNT,
54+
license_key=app_settings.WHOIS_GEOIP_KEY,
55+
host="geolite.info",
56+
)
57+
2258
@staticmethod
2359
def get_cache_key(org_id):
2460
"""
@@ -116,6 +152,55 @@ def is_estimated_location_enabled(self):
116152
org_settings = self.get_org_config_settings(org_id=self.device.organization.pk)
117153
return org_settings.estimated_location_enabled
118154

155+
def process_whois_details(self, ip_address):
156+
"""
157+
Fetch WHOIS details for a given IP address and return only
158+
the relevant information.
159+
"""
160+
ip_client = self.geoip_client()
161+
162+
try:
163+
data = ip_client.city(ip_address=ip_address)
164+
165+
# Catching all possible exceptions raised by the geoip2 client
166+
# and raising them with appropriate messages to be handled by the task
167+
# retry mechanism.
168+
except (
169+
errors.AddressNotFoundError,
170+
errors.AuthenticationError,
171+
errors.OutOfQueriesError,
172+
errors.PermissionRequiredError,
173+
) as e:
174+
exc_type = type(e)
175+
message = EXCEPTION_MESSAGES.get(exc_type)
176+
if exc_type is errors.AddressNotFoundError:
177+
message = message.format(ip_address=ip_address)
178+
raise exc_type(message)
179+
except requests.RequestException as e:
180+
raise e
181+
182+
else:
183+
# The attributes are always present in the response,
184+
# but they can be None, so added fallbacks.
185+
address = {
186+
"city": data.city.name or "",
187+
"country": data.country.name or "",
188+
"continent": data.continent.name or "",
189+
"postal": str(data.postal.code or ""),
190+
}
191+
coordinates = Point(
192+
data.location.longitude, data.location.latitude, srid=4326
193+
)
194+
return {
195+
"isp": data.traits.autonomous_system_organization,
196+
"asn": data.traits.autonomous_system_number,
197+
"timezone": data.location.time_zone,
198+
"address": address,
199+
"coordinates": coordinates,
200+
"cidr": data.traits.network,
201+
"ip_address": ip_address,
202+
}
203+
119204
def _need_whois_lookup(self, new_ip):
120205
"""
121206
This is used to determine if the WHOIS lookup should be triggered

openwisp_controller/config/whois/tasks.py

Lines changed: 45 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import logging
22

3-
import requests
43
from celery import shared_task
5-
from django.contrib.gis.geos import Point
64
from django.db import transaction
7-
from django.utils.translation import gettext as _
85
from geoip2 import errors
9-
from geoip2 import webservice as geoip2_webservice
106
from swapper import load_model
117

128
from openwisp_controller.geo.estimated_location.tasks import manage_estimated_locations
@@ -17,23 +13,6 @@
1713

1814
logger = logging.getLogger(__name__)
1915

20-
EXCEPTION_MESSAGES = {
21-
errors.AddressNotFoundError: _(
22-
"No WHOIS information found for IP address {ip_address}"
23-
),
24-
errors.AuthenticationError: _(
25-
"Authentication failed for GeoIP2 service. "
26-
"Check your OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT and "
27-
"OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY settings."
28-
),
29-
errors.OutOfQueriesError: _(
30-
"Your account has run out of queries for the GeoIP2 service."
31-
),
32-
errors.PermissionRequiredError: _(
33-
"Your account does not have permission to access this service."
34-
),
35-
}
36-
3716

3817
class WHOISCeleryRetryTask(OpenwispCeleryTask):
3918
"""
@@ -54,6 +33,28 @@ def on_failure(self, exc, task_id, args, kwargs, einfo):
5433
return super().on_failure(exc, task_id, args, kwargs, einfo)
5534

5635

36+
def _manage_whois_record(whois_details, whois_instance=None):
37+
"""
38+
Used to update an existing WHOIS instance; else, creates a new one.
39+
Returns the updated or created WHOIS instance along with update fields.
40+
"""
41+
WHOISInfo = load_model("config", "WHOISInfo")
42+
43+
update_fields = []
44+
if whois_instance:
45+
for attr, value in whois_details.items():
46+
if getattr(whois_instance, attr) != value:
47+
update_fields.append(attr)
48+
setattr(whois_instance, attr, value)
49+
if update_fields:
50+
whois_instance.save(update_fields=update_fields)
51+
else:
52+
whois_instance = WHOISInfo(**whois_details)
53+
whois_instance.full_clean()
54+
whois_instance.save()
55+
return whois_instance, update_fields
56+
57+
5758
# device_pk is used when task fails to report for which device failure occurred
5859
@shared_task(
5960
bind=True,
@@ -71,93 +72,35 @@ def fetch_whois_details(self, device_pk, initial_ip_address):
7172
with transaction.atomic():
7273
device = Device.objects.get(pk=device_pk)
7374
new_ip_address = device.last_ip
75+
WHOISService = device.whois_service
76+
7477
# If there is existing WHOIS older record then it needs to be updated
7578
whois_obj = WHOISInfo.objects.filter(ip_address=new_ip_address).first()
76-
if whois_obj and not device.whois_service.is_older(whois_obj.modified):
79+
if whois_obj and not WHOISService.is_older(whois_obj.modified):
7780
return
7881

79-
# Host is based on the db that is used to fetch the details.
80-
# As we are using GeoLite2, 'geolite.info' host is used.
81-
# Refer: https://geoip2.readthedocs.io/en/latest/#sync-web-service-example
82-
ip_client = geoip2_webservice.Client(
83-
account_id=app_settings.WHOIS_GEOIP_ACCOUNT,
84-
license_key=app_settings.WHOIS_GEOIP_KEY,
85-
host="geolite.info",
86-
)
82+
fetched_details = WHOISService.process_whois_details(new_ip_address)
83+
whois_obj, update_fields = _manage_whois_record(fetched_details, whois_obj)
84+
logger.info(f"Successfully fetched WHOIS details for {new_ip_address}.")
8785

88-
try:
89-
data = ip_client.city(ip_address=new_ip_address)
90-
91-
# Catching all possible exceptions raised by the geoip2 client
92-
# and raising them with appropriate messages to be handled by the task
93-
# retry mechanism.
94-
except (
95-
errors.AddressNotFoundError,
96-
errors.AuthenticationError,
97-
errors.OutOfQueriesError,
98-
errors.PermissionRequiredError,
99-
) as e:
100-
exc_type = type(e)
101-
message = EXCEPTION_MESSAGES.get(exc_type)
102-
if exc_type is errors.AddressNotFoundError:
103-
message = message.format(ip_address=new_ip_address)
104-
raise exc_type(message)
105-
except requests.RequestException as e:
106-
raise e
107-
108-
else:
109-
# The attributes are always present in the response,
110-
# but they can be None, so added fallbacks.
111-
address = {
112-
"city": data.city.name or "",
113-
"country": data.country.name or "",
114-
"continent": data.continent.name or "",
115-
"postal": str(data.postal.code or ""),
116-
}
117-
coordinates = Point(
118-
data.location.longitude, data.location.latitude, srid=4326
119-
)
120-
details = {
121-
"isp": data.traits.autonomous_system_organization,
122-
"asn": data.traits.autonomous_system_number,
123-
"timezone": data.location.time_zone,
124-
"address": address,
125-
"coordinates": coordinates,
126-
"cidr": data.traits.network,
127-
"ip_address": new_ip_address,
128-
}
129-
update_fields = []
130-
if whois_obj:
131-
for attr, value in details.items():
132-
if getattr(whois_obj, attr) != value:
133-
update_fields.append(attr)
134-
setattr(whois_obj, attr, value)
135-
if update_fields:
136-
whois_obj.save(update_fields=update_fields)
137-
else:
138-
whois_obj = WHOISInfo(**details)
139-
whois_obj.full_clean()
140-
whois_obj.save()
141-
logger.info(f"Successfully fetched WHOIS details for {new_ip_address}.")
142-
143-
if device._get_organization__config_settings().estimated_location_enabled:
144-
# the estimated location task should not run if old record is updated
145-
# and location related fields are not updated
146-
if update_fields and not any(
147-
i in update_fields for i in ["address", "coordinates"]
148-
):
149-
return
150-
manage_estimated_locations.delay(
151-
device_pk=device_pk, ip_address=new_ip_address
152-
)
153-
154-
# delete WHOIS record for initial IP if no devices are linked to it
155-
if (
156-
not Device.objects.filter(_is_deactivated=False)
157-
.filter(last_ip=initial_ip_address)
158-
.exists()
86+
if device._get_organization__config_settings().estimated_location_enabled:
87+
# the estimated location task should not run if old record is updated
88+
# and location related fields are not updated
89+
if update_fields and not any(
90+
i in update_fields for i in ["address", "coordinates"]
15991
):
160-
delete_whois_record(ip_address=initial_ip_address)
92+
return
93+
manage_estimated_locations.delay(
94+
device_pk=device_pk, ip_address=new_ip_address
95+
)
96+
97+
# delete WHOIS record for initial IP if no devices are linked to it
98+
if (
99+
not Device.objects.filter(_is_deactivated=False)
100+
.filter(last_ip=initial_ip_address)
101+
.exists()
102+
):
103+
delete_whois_record(ip_address=initial_ip_address)
161104

162105

163106
@shared_task

openwisp_controller/config/whois/tests/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ class TestWHOISTransaction(
343343
CreateWHOISMixin, WHOISTransactionMixin, TransactionTestCase
344344
):
345345
_WHOIS_GEOIP_CLIENT = (
346-
"openwisp_controller.config.whois.tasks.geoip2_webservice.Client"
346+
"openwisp_controller.config.whois.service.geoip2_webservice.Client"
347347
)
348348
_WHOIS_TASKS_INFO_LOGGER = "openwisp_controller.config.whois.tasks.logger.info"
349349
_WHOIS_TASKS_WARN_LOGGER = "openwisp_controller.config.whois.tasks.logger.warning"

0 commit comments

Comments
 (0)