Skip to content

Commit d7a2198

Browse files
committed
[feature] Estimated location: Setup and Management #1034
Closes #1034 Signed-off-by: DragnEmperor <[email protected]>
1 parent 5c93b19 commit d7a2198

30 files changed

+1368
-147
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ the OpenWISP architecture.
4949
user/openvpn.rst
5050
user/subnet-division-rules.rst
5151
user/whois.rst
52+
user/estimated-location.rst
5253
user/rest-api.rst
5354
user/settings.rst
5455

docs/user/estimated-location.rst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Estimated Location
2+
==================
3+
4+
.. important::
5+
6+
The **Estimated Location** feature is **disabled by default**.
7+
8+
Before enabling it, the :doc:`WHOIS Lookup feature <whois>` must be
9+
enabled. Then set
10+
:ref:`OPENWISP_CONTROLLER_ESTIMATED_LOCATION_ENABLED` to ``True``
11+
12+
.. contents:: **Table of contents**:
13+
:depth: 1
14+
:local:
15+
16+
Overview
17+
--------
18+
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.
22+
23+
Trigger Conditions
24+
------------------
25+
26+
Estimated Location is triggered when:
27+
28+
- A **fresh WHOIS lookup** is performed for a device.
29+
- Or when a WHOIS record already exists for the device’s IP **and**:
30+
31+
- The device’s last IP address is **public**.
32+
- WHOIS lookup and Estimated Location is **enabled** for the device’s
33+
organization.
34+
35+
Behavior
36+
--------
37+
38+
The system will **attach the already existing matching location** of
39+
another device with same ip to the current device if:
40+
41+
- Only one device is found with that IP and it has a location.
42+
- The current device **has no location** or that location is
43+
**estimated**.
44+
45+
If there are multiple devices with location for the same IP, the system
46+
will **not attach any location** to the current device and a notification
47+
will be sent suggesting the user to manually assign/create a location for
48+
the device.
49+
50+
If there is **no matching location**, a new estimated location is created
51+
or the existing one is updated using coordinates from the WHOIS record,
52+
but only if the existing location is estimated.
53+
54+
If two devices share the same IP address and are assigned to the same
55+
location, and the last IP of one of the devices is updated, the system
56+
will create a new estimated location for that device.

docs/user/settings.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,3 +795,20 @@ Maxmind Account ID required for the :doc:`WHOIS Lookup feature <whois>`.
795795
============ =======
796796

797797
Maxmind License Key required for the :doc:`WHOIS Lookup feature <whois>`.
798+
799+
.. _openwisp_controller_whois_estimated_location_enabled:
800+
801+
``OPENWISP_CONTROLLER_WHOIS_ESTIMATED_LOCATION_ENABLED``
802+
--------------------------------------------------------
803+
804+
============ =========
805+
**type**: ``bool``
806+
**default**: ``False``
807+
============ =========
808+
809+
Allows enabling the optional :doc:`Estimated Location feature
810+
<estimated-location>`.
811+
812+
.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.3/estimated-location-setting.png
813+
:target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.3/estimated-location-setting.png
814+
:alt: Estimated Location setting

docs/user/whois.rst

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,23 @@ A WHOIS lookup is triggered automatically when:
4040
However, the lookup will only run if **all** the following conditions are
4141
met:
4242

43+
- The device is either **newly created** or has a **changed last IP**.
4344
- The device's last IP address is **public**.
4445
- There is **no existing WHOIS record** for that IP.
4546
- WHOIS lookup is **enabled** for the device's organization.
4647

47-
Behavior with Shared IP Addresses
48-
---------------------------------
48+
Managing WHOIS Records
49+
----------------------
4950

50-
If multiple devices share the same public IP address and one of them
51-
switches to a different IP, the following occurs:
52-
53-
- A lookup is triggered for the **new IP**.
54-
- The WHOIS record for the **old IP** is deleted.
55-
- The next time a device still using the old IP fetches its checksum, a
56-
new lookup is triggered, ensuring up-to-date data.
51+
If a device updates its last IP address, lookup is triggered for the **new
52+
IP** and the **WHOIS record for the old IP** is deleted if no active
53+
devices are associated with that IP address.
5754

5855
.. note::
5956

6057
When a device with an associated WHOIS record is deleted, its WHOIS
61-
record is automatically removed.
58+
record is automatically removed only if no active devices are
59+
associated with that IP address.
6260

6361
.. _controller_setup_whois_lookup:
6462

@@ -79,6 +77,26 @@ Setup Instructions
7977
- Set :ref:`OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT` to **Account ID**.
8078
- Set :ref:`OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY` to **License Key**.
8179

80+
6. Restart the application/containers if using ansible-openwisp2 or
81+
docker.
82+
7. Run the ``clear_last_ip`` management command to clear the last IP
83+
address of **all active devices across organizations**.
84+
85+
- If using ansible-openwisp2 (default directory is /opt/openwisp2,
86+
unless changed in Ansible playbook configuration):
87+
88+
.. code-block:: bash
89+
90+
source /opt/openwisp2/env/bin/activate
91+
python /opt/openwisp2/src/manage.py clear_last_ip
92+
93+
- If using docker:
94+
95+
.. code-block:: bash
96+
97+
docker exec -it <openwisp_container_name> sh
98+
python manage.py clear_last_ip
99+
82100
Viewing WHOIS Lookup Data
83101
-------------------------
84102

openwisp_controller/config/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,7 @@ def get_fields(self, request, obj=None):
13841384
if app_settings.REGISTRATION_ENABLED:
13851385
fields += ["registration_enabled", "shared_secret"]
13861386
if app_settings.WHOIS_CONFIGURED:
1387-
fields += ["whois_enabled"]
1387+
fields += ["whois_enabled", "estimated_location_enabled"]
13881388
fields += ["context"]
13891389
return fields
13901390

openwisp_controller/config/base/device.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def save(self, *args, **kwargs):
287287
state_adding = self._state.adding
288288
super().save(*args, **kwargs)
289289
if app_settings.WHOIS_CONFIGURED:
290-
self._check_last_ip()
290+
self._check_last_ip(creating=state_adding)
291291
if state_adding and self.group and self.group.templates.exists():
292292
self.create_default_config()
293293
# The value of "self._state.adding" will always be "False"
@@ -510,9 +510,13 @@ def whois_service(self):
510510
"""
511511
return WHOISService(self)
512512

513-
def _check_last_ip(self):
514-
"""Trigger WHOIS lookup if last_ip is not deferred."""
513+
def _check_last_ip(self, creating=False):
514+
"""
515+
Process details and location related to last_ip if last_ip has
516+
changed or is being set for the first time.
517+
"""
515518
if self._initial_last_ip == models.DEFERRED:
516519
return
517-
self.whois_service.trigger_whois_lookup()
520+
if creating or self.last_ip != self._initial_last_ip:
521+
self.whois_service.process_ip_data_and_location()
518522
self._initial_last_ip = self.last_ip

openwisp_controller/config/base/multitenancy.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class AbstractOrganizationConfigSettings(UUIDModel):
3939
fallback=app_settings.WHOIS_ENABLED,
4040
verbose_name=_("WHOIS Enabled"),
4141
)
42+
estimated_location_enabled = FallbackBooleanChoiceField(
43+
help_text=_("Whether the estimated location feature is enabled"),
44+
fallback=app_settings.ESTIMATED_LOCATION_ENABLED,
45+
verbose_name=_("Estimated Location Enabled"),
46+
)
4247
context = JSONField(
4348
blank=True,
4449
default=dict,
@@ -71,6 +76,15 @@ def clean(self):
7176
)
7277
}
7378
)
79+
if not self.whois_enabled and self.estimated_location_enabled:
80+
raise ValidationError(
81+
{
82+
"estimated_location_enabled": _(
83+
"Estimated Location feature requires "
84+
"WHOIS Lookup feature to be enabled."
85+
)
86+
}
87+
)
7488
return super().clean()
7589

7690
def save(

openwisp_controller/config/base/whois.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from ipaddress import ip_address, ip_network
22

3+
from django.contrib.gis.db.models import PointField
34
from django.core.cache import cache
45
from django.core.exceptions import ValidationError
56
from django.db import models, transaction
67
from django.utils.translation import gettext_lazy as _
78
from jsonfield import JSONField
9+
from swapper import load_model
810

911
from openwisp_utils.base import TimeStampedEditableModel
1012

@@ -51,6 +53,12 @@ class AbstractWHOISInfo(TimeStampedEditableModel):
5153
blank=True,
5254
help_text=_("CIDR"),
5355
)
56+
coordinates = PointField(
57+
null=True,
58+
blank=True,
59+
help_text=_("Coordinates"),
60+
srid=4326,
61+
)
5462

5563
class Meta:
5664
abstract = True
@@ -74,6 +82,20 @@ def clean(self):
7482
raise ValidationError(
7583
{"cidr": _("Invalid CIDR format: %(error)s") % {"error": str(e)}}
7684
)
85+
86+
if self.coordinates:
87+
if not (-90 <= self.coordinates.y <= 90):
88+
raise ValidationError(
89+
{"coordinates": _("Latitude must be between -90 and 90 degrees.")}
90+
)
91+
if not (-180 <= self.coordinates.x <= 180):
92+
raise ValidationError(
93+
{
94+
"coordinates": _(
95+
"Longitude must be between -180 and 180 degrees."
96+
)
97+
}
98+
)
7799
return super().clean()
78100

79101
@staticmethod
@@ -82,8 +104,18 @@ def device_whois_info_delete_handler(instance, **kwargs):
82104
Delete WHOIS information for a device when the last IP address is removed or
83105
when device is deleted.
84106
"""
85-
if instance._get_organization__config_settings().whois_enabled:
86-
transaction.on_commit(lambda: delete_whois_record.delay(instance.last_ip))
107+
Device = load_model("config", "Device")
108+
109+
last_ip = instance.last_ip
110+
existing_devices = Device.objects.filter(_is_deactivated=False).filter(
111+
last_ip=last_ip
112+
)
113+
if (
114+
last_ip
115+
and instance._get_organization__config_settings().whois_enabled
116+
and not existing_devices.exists()
117+
):
118+
transaction.on_commit(lambda: delete_whois_record.delay(last_ip))
87119

88120
# this method is kept here instead of in OrganizationConfigSettings because
89121
# currently the caching is used only for WHOIS feature
@@ -113,3 +145,28 @@ def formatted_address(self):
113145
],
114146
)
115147
)
148+
149+
@property
150+
def _location_name(self):
151+
"""
152+
Used to get location name based on the address and IP.
153+
"""
154+
address = self.formatted_address
155+
if address:
156+
parts = [part.strip() for part in address.split(",")[:2] if part.strip()]
157+
location = ", ".join(parts)
158+
return _(f"{location} (Estimated Location: {self.ip_address})")
159+
return _(f"Estimated Location: {self.ip_address}")
160+
161+
def _get_defaults_for_estimated_location(self):
162+
"""
163+
Used to get default values for creating or updating
164+
an estimated location based on the WHOIS information.
165+
"""
166+
return {
167+
"name": self._location_name,
168+
"type": "outdoor",
169+
"is_mobile": False,
170+
"geometry": self.coordinates,
171+
"address": self.formatted_address,
172+
}

openwisp_controller/config/controller/views.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,6 @@ def get(self, request, pk):
153153
# updates cache if ip addresses changed
154154
if updated:
155155
self.update_device_cache(device)
156-
# When update fields are present then save() will run the WHOIS
157-
# lookup. But if there are no update fields, we still want to
158-
# trigger the WHOIS lookup if there is no record for the device's
159-
# last_ip.
160-
elif (
161-
app_settings.WHOIS_CONFIGURED
162-
and not device.whois_service.get_device_whois_info()
163-
):
164-
device.whois_service.trigger_whois_lookup()
165156
checksum_requested.send(
166157
sender=device.__class__, instance=device, request=request
167158
)

openwisp_controller/config/management/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)