Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
venv/
.git/
.github/
.tox/
htmlcov/
build/
dist/
*.egg-info/
Comment on lines +1 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add missing common Python artifact exclusions.

The .dockerignore file is missing several standard Python cache and artifact directories that should be excluded from Docker builds:

  • __pycache__/ (essential—generated during any Python execution)
  • .pytest_cache/
  • .mypy_cache/
  • *.pyc
  • .coverage

Additionally, if environment files (.env, .env.local, etc.) may be created or exist in the build context, consider excluding them to prevent accidental inclusion of secrets in the image.

🐳 Proposed additions to .dockerignore
 venv/
 .git/
 .github/
 .tox/
 htmlcov/
 build/
 dist/
 *.egg-info/
+__pycache__/
+.pytest_cache/
+.mypy_cache/
+*.pyc
+.coverage
+.env
+.env.local
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
venv/
.git/
.github/
.tox/
htmlcov/
build/
dist/
*.egg-info/
venv/
.git/
.github/
.tox/
htmlcov/
build/
dist/
*.egg-info/
__pycache__/
.pytest_cache/
.mypy_cache/
*.pyc
.coverage
.env
.env.local
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.dockerignore around lines 1 - 8, The .dockerignore file is missing several
standard Python cache and artifact directories that should be excluded from
Docker builds to reduce image size and prevent unnecessary artifacts. Add the
following entries to the .dockerignore file: __pycache__/ for Python bytecode
cache directories, .pytest_cache/ for pytest artifacts, .mypy_cache/ for mypy
type checking cache, *.pyc for compiled Python files, and .coverage for code
coverage reports. Additionally, consider adding environment file patterns like
.env and .env.local to prevent accidental inclusion of secrets or local
configuration in the Docker image.

62 changes: 46 additions & 16 deletions docs/user/metrics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,25 @@ Mobile Signal Strength
:target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-strength.png
:align: center

================== =====================================
**collection**: :doc:`OpenWrt Monitoring Agent
</openwrt-monitoring-agent/index>`
**measurement**: ``signal_strength``
================== ==========================================================================
**collection**: :doc:`OpenWrt Monitoring Agent </openwrt-monitoring-agent/index>`
**measurement**: ``signal``
**type**: ``float``
**fields**: ``signal_strength``, ``signal_power``
**tags**: .. code-block:: python

{
"organization_id": "<organization-id-of-the-related-device>",
"ifname": "<interface-name>",
# optional
"location_id": "<location-id-of-the-related-device-if-present>",
"floorplan_id": "<floorplan-id-of-the-related-device-if-present>",
}
**configuration**: ``signal_strength``
**charts**: ``signal_strength``
================== =====================================
================== ==========================================================================

If a device has multiple mobile interfaces, a separate chart will be created for each interface, with its name appended to the chart title (e.g. ``Signal Strength (RSSI): mobile0``).

.. _mobile_signal_quality:

Expand All @@ -226,15 +236,25 @@ Mobile Signal Quality
:target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/signal-quality.png
:align: center

================== ======================================
**collection**: :doc:`OpenWrt Monitoring Agent
</openwrt-monitoring-agent/index>`
**measurement**: ``signal_quality``
================== ==========================================================================
**collection**: :doc:`OpenWrt Monitoring Agent </openwrt-monitoring-agent/index>`
**measurement**: ``signal``
**type**: ``float``
**fields**: ``signal_quality``, ``signal_quality``
**fields**: ``signal_quality``, ``snr``
**tags**: .. code-block:: python

{
"organization_id": "<organization-id-of-the-related-device>",
"ifname": "<interface-name>",
# optional
"location_id": "<location-id-of-the-related-device-if-present>",
"floorplan_id": "<floorplan-id-of-the-related-device-if-present>",
}
**configuration**: ``signal_quality``
**charts**: ``signal_quality``
================== ======================================
================== ==========================================================================

If a device has multiple mobile interfaces, a separate chart will be created for each interface, with its name appended to the chart title (e.g. ``Signal Quality (RSRQ): mobile0``).

.. _mobile_access_technology_in_use:

Expand All @@ -245,15 +265,25 @@ Mobile Access Technology in Use
:target: https://raw.githubusercontent.com/openwisp/openwisp-monitoring/docs/docs/access-technology.png
:align: center

================== ==================================
**collection**: :doc:`OpenWrt Monitoring Agent
</openwrt-monitoring-agent/index>`
**measurement**: ``access_tech``
================== ==========================================================================
**collection**: :doc:`OpenWrt Monitoring Agent </openwrt-monitoring-agent/index>`
**measurement**: ``signal``
**type**: ``int``
**fields**: ``access_tech``
**tags**: .. code-block:: python

{
"organization_id": "<organization-id-of-the-related-device>",
"ifname": "<interface-name>",
# optional
"location_id": "<location-id-of-the-related-device-if-present>",
"floorplan_id": "<floorplan-id-of-the-related-device-if-present>",
}
**configuration**: ``access_tech``
**charts**: ``access_tech``
================== ==================================
================== ==========================================================================

If a device has multiple mobile interfaces, a separate chart will be created for each interface, with its name appended to the chart title (e.g. ``Access Technology: mobile0``).

.. _iperf3:

Expand Down
6 changes: 3 additions & 3 deletions openwisp_monitoring/db/backends/influxdb/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,22 @@
"SELECT ROUND(MEAN(signal_strength)) AS signal_strength, "
"ROUND(MEAN(signal_power)) AS signal_power FROM {key} WHERE "
"time >= '{time}' {end_date} AND content_type = '{content_type}' AND "
"object_id = '{object_id}' GROUP BY time(1d)"
"object_id = '{object_id}' AND ifname = '{ifname}' GROUP BY time(1d)"
)
},
"signal_quality": {
"influxdb": (
"SELECT ROUND(MEAN(signal_quality)) AS signal_quality, "
"ROUND(MEAN(snr)) AS signal_to_noise_ratio FROM {key} WHERE "
"time >= '{time}' {end_date} AND content_type = '{content_type}' AND "
"object_id = '{object_id}' GROUP BY time(1d)"
"object_id = '{object_id}' AND ifname = '{ifname}' GROUP BY time(1d)"
)
},
"access_tech": {
"influxdb": (
"SELECT MODE(access_tech) AS access_tech FROM {key} WHERE "
"time >= '{time}' {end_date} AND content_type = '{content_type}' AND "
"object_id = '{object_id}' GROUP BY time(1d)"
"object_id = '{object_id}' AND ifname = '{ifname}' GROUP BY time(1d)"
)
},
"bandwidth": {
Expand Down
65 changes: 65 additions & 0 deletions openwisp_monitoring/device/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,71 @@ def test_mobile_charts(self):
)
self.assertEqual(self.chart_queryset.count(), charts_count + 3)

@tag("flaky_with_udp_writes")
def test_multiple_mobile_charts(self):
org = self._create_org()
device = self._create_device(organization=org)
charts_count = self.chart_queryset.count()
data = {
"type": "DeviceMonitoring",
"interfaces": [
{
"name": "mobile0",
"mac": "00:00:00:00:00:00",
"mtu": 1900,
"multicast": True,
"txqueuelen": 1000,
"type": "modem-manager",
"up": True,
"mobile": {
"connection_status": "connected",
"imei": "300000001234567",
"manufacturer": "Sierra Wireless, Incorporated",
"model": "MC7430",
"operator_code": "50502",
"operator_name": "YES OPTUS",
"power_status": "on",
"signal": {
"lte": {"rsrp": -75, "rsrq": -8, "rssi": -51, "snr": 13},
},
},
},
{
"name": "mobile1",
"mac": "00:00:00:00:00:01",
"mtu": 1900,
"multicast": True,
"txqueuelen": 1000,
"type": "modem-manager",
"up": True,
"mobile": {
"connection_status": "connected",
"imei": "300000001234568",
"manufacturer": "Sierra Wireless, Incorporated",
"model": "MC7430",
"operator_code": "50502",
"operator_name": "YES OPTUS",
"power_status": "on",
"signal": {
"lte": {"rsrp": -80, "rsrq": -10, "rssi": -55, "snr": 11},
},
},
},
],
}
self._post_data(device.id, device.key, data)
response = self.client.get(self._url(device.pk.hex, device.key))
self.assertEqual(response.status_code, 200)
charts = response.data["charts"]
self.assertEqual(self.chart_queryset.count(), charts_count + 6)
chart_titles = [chart["title"] for chart in charts]
self.assertIn("Signal Strength (RSSI): mobile0", chart_titles)
self.assertIn("Signal Strength (RSSI): mobile1", chart_titles)
self.assertIn("Signal Quality (RSRQ): mobile0", chart_titles)
self.assertIn("Signal Quality (RSRQ): mobile1", chart_titles)
self.assertIn("Access Technology: mobile0", chart_titles)
self.assertIn("Access Technology: mobile1", chart_titles)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Per-interface mobile charts display aggregated (incorrect) data.

This test verifies chart titles and counts, but not that each interface's chart returns interface-specific data. The signal_strength, signal_quality, and access_tech InfluxDB query templates in openwisp_monitoring/db/backends/influxdb/queries.py filter only by content_type and object_id — they do not filter by ifname, unlike the traffic and wifi_clients query templates (which include AND ifname = '{ifname}').

With multiple mobile interfaces, each per-interface chart runs the same query, so they aggregate data across all interfaces and return identical values. For this test's data, both the mobile0 and mobile1 signal-strength charts would show the mean of -51 and -55 (-53) instead of -51 and -55 respectively, defeating the purpose of separate per-interface charts.

Consider asserting per-interface summary values (e.g. mobile0 RSSI == -51.0, mobile1 RSSI == -55.0) and adding AND ifname = '{ifname}' to the three mobile query templates so the charts are truly scoped per interface.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


# This test reads chart summaries immediately after posting data. That is
# unreliable with UDP writes.
@tag("flaky_with_udp_writes")
Expand Down
24 changes: 19 additions & 5 deletions openwisp_monitoring/device/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ def write(self, data, time=None, current=False):
ifname = interface["name"]
if "mobile" in interface:
self._write_mobile_signal(
interface, ifname, ct, self.device_data.pk, current, time=time
interface,
ifname,
ct,
self.device_data.pk,
current,
time=time,
extra_tags=device_extra_tags,
)
ifstats = interface.get("statistics", {})
# Explicitly stated None to avoid skipping in case the stats are zero
Expand Down Expand Up @@ -192,7 +198,9 @@ def _get_mobile_signal_type(self, signal):
if tech in signal:
return tech

def _write_mobile_signal(self, interface, ifname, ct, pk, current=False, time=None):
def _write_mobile_signal(
self, interface, ifname, ct, pk, current=False, time=None, extra_tags=None
):
access_type = self._get_mobile_signal_type(interface["mobile"].get("signal"))
if not access_type:
return
Expand All @@ -210,13 +218,15 @@ def _write_mobile_signal(self, interface, ifname, ct, pk, current=False, time=No
if signal_strength is not None:
signal_strength = float(signal_strength)
if signal_strength is not None or signal_power is not None:
name = f"{ifname} signal strength"
metric, created = Metric._get_or_create(
object_id=self.device_data.pk,
content_type_id=ct.id,
configuration="signal_strength",
name="signal strength",
name=name,
key="signal",
main_tags={"ifname": Metric._makekey(ifname)},
extra_tags=extra_tags,
)
self._append_metric_data(
metric, signal_strength, current, time=time, extra_values=extra_values
Expand All @@ -239,27 +249,31 @@ def _write_mobile_signal(self, interface, ifname, ct, pk, current=False, time=No
if signal_quality is not None:
signal_quality = float(signal_quality)
if snr is not None or signal_quality is not None:
name = f"{ifname} signal quality"
metric, created = Metric._get_or_create(
object_id=self.device_data.pk,
content_type_id=ct.id,
configuration="signal_quality",
name="signal quality",
name=name,
key="signal",
main_tags={"ifname": Metric._makekey(ifname)},
extra_tags=extra_tags,
)
self._append_metric_data(
metric, signal_quality, current, time=time, extra_values=extra_values
)
if created:
self._create_signal_quality_chart(metric)
# create access technology chart
name = f"{ifname} access technology"
metric, created = Metric._get_or_create(
object_id=self.device_data.pk,
content_type_id=ct.id,
configuration="access_tech",
name="access technology",
name=name,
key="signal",
main_tags={"ifname": Metric._makekey(ifname)},
extra_tags=extra_tags,
)
self._append_metric_data(
metric,
Expand Down
6 changes: 3 additions & 3 deletions openwisp_monitoring/monitoring/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ def _get_access_tech():
"type": "scatter",
"fill": "none",
"yaxis": {"zeroline": False},
"title": _("Signal Strength (RSSI)"),
"title": _("Signal Strength (RSSI): {ifname}"),
"colors": (DEFAULT_COLORS[3], DEFAULT_COLORS[0]),
"description": _(
"Signal Strength (RSSI) and Signal Power (RSRP), measured in dBm."
Expand All @@ -598,7 +598,7 @@ def _get_access_tech():
"type": "scatter",
"fill": "none",
"yaxis": {"zeroline": False},
"title": _("Signal Quality (RSRQ)"),
"title": _("Signal Quality (RSRQ): {ifname}"),
"colors": (DEFAULT_COLORS[3], DEFAULT_COLORS[0]),
"description": _(
_(
Expand All @@ -623,7 +623,7 @@ def _get_access_tech():
"charts": {
"access_tech": {
"type": "bar",
"title": _("Access Technology"),
"title": _("Access Technology: {ifname}"),
"description": _(
_(
"Shows the access technology (LTE, UTMS, CDMA1x, etc.) "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Manually created
# Updates signal metric names to include the interface name (ifname) prefix,
# matching the new naming convention introduced by writer.py which now uses
# "{ifname} signal strength", "{ifname} signal quality", "{ifname} access technology"
# instead of the old static names "signal strength", "signal quality", "access technology".

import logging

from django.db import migrations

CHUNK_SIZE = 1000

logger = logging.getLogger(__name__)

SIGNAL_CONFIGURATIONS = {
"signal_strength": "signal strength",
"signal_quality": "signal quality",
"access_tech": "access technology",
}


def forward_migration(apps, schema_editor):
"""
Update old signal metric names to include the interface name.
e.g. "signal strength" → "mobile0 signal strength"
"""
Metric = apps.get_model("monitoring", "Metric")
updated_metrics = []
for configuration, old_suffix in SIGNAL_CONFIGURATIONS.items():
metric_qs = Metric.objects.filter(
configuration=configuration,
name=old_suffix,
)
Comment on lines +27 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n openwisp_monitoring/monitoring/migrations/0014_update_signal_metric_names.py

Repository: openwisp/openwisp-monitoring

Length of output: 3942


🏁 Script executed:

# Check if project has multi-database configuration
find . -name "settings*.py" -o -name "settings" -type d | head -5

Repository: openwisp/openwisp-monitoring

Length of output: 356


🏁 Script executed:

# Check main settings for database configuration
grep -A 20 "DATABASES" openwisp_monitoring/settings.py | head -30

Repository: openwisp/openwisp-monitoring

Length of output: 166


🏁 Script executed:

# Check Metric model for tenant isolation and data integrity concerns
grep -n "class Metric" openwisp_monitoring/monitoring/models/*.py

Repository: openwisp/openwisp-monitoring

Length of output: 242


🏁 Script executed:

# Find the Metric model
find . -name "*.py" -path "*/monitoring/*" | xargs grep -l "class Metric" | head -5

Repository: openwisp/openwisp-monitoring

Length of output: 251


🏁 Script executed:

# Check for database configuration in test settings
cat openwisp_monitoring/settings.py | head -50

Repository: openwisp/openwisp-monitoring

Length of output: 956


🏁 Script executed:

# Check the Metric model for structure and tenant isolation
ast-grep outline openwisp_monitoring/monitoring/models.py | head -100

Repository: openwisp/openwisp-monitoring

Length of output: 375


🏁 Script executed:

# Check test settings for database configuration
grep -A 20 "DATABASES" tests/openwisp2/settings.py

Repository: openwisp/openwisp-monitoring

Length of output: 1205


🏁 Script executed:

# Check if other migrations in the same app use db_alias pattern
find openwisp_monitoring/monitoring/migrations -name "*.py" | xargs grep -l "schema_editor.connection.alias" | head -5

Repository: openwisp/openwisp-monitoring

Length of output: 166


🏁 Script executed:

# Check if other RunPython migrations in the project follow the db_alias pattern
find . -path "*/migrations/*.py" -name "*.py" | xargs grep -B5 -A10 "def.*migration.*apps.*schema_editor" | grep -A10 "schema_editor.connection.alias" | head -40

Repository: openwisp/openwisp-monitoring

Length of output: 166


🏁 Script executed:

# Check if project documentation or comments mention multi-database support
rg -i "multi.?db|database.*alias|schema_editor" --type md -A 3 -B 3 | head -50

Repository: openwisp/openwisp-monitoring

Length of output: 166


🏁 Script executed:

# Check other files in the monitoring app for any existing migrations patterns
ls -la openwisp_monitoring/monitoring/migrations/ | head -20

Repository: openwisp/openwisp-monitoring

Length of output: 1607


🏁 Script executed:

# Check migration 0013 for comparison - does it use db_alias?
sed -n '1,100p' openwisp_monitoring/monitoring/migrations/0013_replace_jsonfield_with_django_builtin.py

Repository: openwisp/openwisp-monitoring

Length of output: 1974


🏁 Script executed:

# Check migration 0012 for RunPython usage
cat openwisp_monitoring/monitoring/migrations/0012_migrate_signal_metrics.py

Repository: openwisp/openwisp-monitoring

Length of output: 1094


🏁 Script executed:

# Check the influxdb migration helper
cat openwisp_monitoring/monitoring/migrations/influxdb/influxdb_alter_structure_0006.py | head -80

Repository: openwisp/openwisp-monitoring

Length of output: 2606


🏁 Script executed:

# Check Django documentation pattern - look for any comments in the codebase about migration best practices
rg -i "runpython|schema_editor" --type py openwisp_monitoring/monitoring/migrations/ -A 2 -B 2 | head -80

Repository: openwisp/openwisp-monitoring

Length of output: 6814


🏁 Script executed:

# Verify the exact lines mentioned in the review comment one more time
sed -n '22,72p' openwisp_monitoring/monitoring/migrations/0014_update_signal_metric_names.py | cat -n

Repository: openwisp/openwisp-monitoring

Length of output: 2852


Use the migration connection alias for all ORM reads/writes.

Lines 30, 40, 46, 59, 67, and 70 query and update through Metric.objects directly. In RunPython, this can hit the wrong database in multi-DB setups. Use schema_editor.connection.alias consistently for both queryset iteration and bulk_update.

Suggested patch
 def forward_migration(apps, schema_editor):
+    db_alias = schema_editor.connection.alias
     Metric = apps.get_model("monitoring", "Metric")
     updated_metrics = []
     for configuration, old_suffix in SIGNAL_CONFIGURATIONS.items():
-        metric_qs = Metric.objects.filter(
+        metric_qs = Metric.objects.using(db_alias).filter(
             configuration=configuration,
             name=old_suffix,
         )
         for metric in metric_qs.iterator(chunk_size=CHUNK_SIZE):
@@
                 updated_metrics.append(metric)
                 if len(updated_metrics) >= CHUNK_SIZE:
-                    Metric.objects.bulk_update(updated_metrics, fields=["name"])
+                    Metric.objects.using(db_alias).bulk_update(
+                        updated_metrics, fields=["name"]
+                    )
@@
     if updated_metrics:
-        Metric.objects.bulk_update(updated_metrics, fields=["name"])
+        Metric.objects.using(db_alias).bulk_update(updated_metrics, fields=["name"])
@@
 def reverse_migration(apps, schema_editor):
+    db_alias = schema_editor.connection.alias
     Metric = apps.get_model("monitoring", "Metric")
     updated_metrics = []
     for configuration, old_suffix in SIGNAL_CONFIGURATIONS.items():
-        metric_qs = Metric.objects.filter(configuration=configuration)
+        metric_qs = Metric.objects.using(db_alias).filter(configuration=configuration)
         for metric in metric_qs.iterator(chunk_size=CHUNK_SIZE):
@@
                 updated_metrics.append(metric)
                 if len(updated_metrics) >= CHUNK_SIZE:
-                    Metric.objects.bulk_update(updated_metrics, fields=["name"])
+                    Metric.objects.using(db_alias).bulk_update(
+                        updated_metrics, fields=["name"]
+                    )
                     updated_metrics = []
     if updated_metrics:
-        Metric.objects.bulk_update(updated_metrics, fields=["name"])
+        Metric.objects.using(db_alias).bulk_update(updated_metrics, fields=["name"])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Metric = apps.get_model("monitoring", "Metric")
updated_metrics = []
for configuration, old_suffix in SIGNAL_CONFIGURATIONS.items():
metric_qs = Metric.objects.filter(
configuration=configuration,
name=old_suffix,
)
Metric = apps.get_model("monitoring", "Metric")
updated_metrics = []
for configuration, old_suffix in SIGNAL_CONFIGURATIONS.items():
metric_qs = Metric.objects.using(db_alias).filter(
configuration=configuration,
name=old_suffix,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_monitoring/monitoring/migrations/0014_update_signal_metric_names.py`
around lines 27 - 33, The migration uses Metric.objects directly for queries and
bulk_update operations without specifying the correct database connection. In
multi-database setups, this can cause operations to hit the wrong database. Fix
this by chaining .using(schema_editor.connection.alias) to all Metric.objects
method calls throughout the migration (including filter operations at the lines
you mentioned and the bulk_update operation), ensuring all database operations
use the correct connection alias provided by the schema_editor.

for metric in metric_qs.iterator(chunk_size=CHUNK_SIZE):
ifname = metric.main_tags.get("ifname", "")
if ifname:
metric.name = f"{ifname} {old_suffix}"
updated_metrics.append(metric)
if len(updated_metrics) >= CHUNK_SIZE:
Metric.objects.bulk_update(updated_metrics, fields=["name"])
logger.info(
f"Bulk updated {len(updated_metrics)} signal metric names."
)
updated_metrics = []
if updated_metrics:
Metric.objects.bulk_update(updated_metrics, fields=["name"])
logger.info(f"Bulk updated {len(updated_metrics)} signal metric names.")
logger.info("Signal metric name migration (forward) completed.")


def reverse_migration(apps, schema_editor):
"""
Revert signal metric names back to the old static names.
e.g. "mobile0 signal strength" → "signal strength"
"""
Metric = apps.get_model("monitoring", "Metric")
updated_metrics = []
for configuration, old_suffix in SIGNAL_CONFIGURATIONS.items():
metric_qs = Metric.objects.filter(configuration=configuration)
for metric in metric_qs.iterator(chunk_size=CHUNK_SIZE):
ifname = metric.main_tags.get("ifname", "")
expected_new_name = f"{ifname} {old_suffix}" if ifname else old_suffix
if metric.name == expected_new_name:
metric.name = old_suffix
updated_metrics.append(metric)
if len(updated_metrics) >= CHUNK_SIZE:
Metric.objects.bulk_update(updated_metrics, fields=["name"])
updated_metrics = []
if updated_metrics:
Metric.objects.bulk_update(updated_metrics, fields=["name"])
logger.info("Signal metric name migration (reverse) completed.")


class Migration(migrations.Migration):
dependencies = [("monitoring", "0013_replace_jsonfield_with_django_builtin")]

operations = [
migrations.RunPython(
forward_migration,
reverse_code=reverse_migration,
)
]
Loading
Loading