Skip to content

Commit 0366a4b

Browse files
[change] Set device status to unknown if critical check is disaled/deleted #576
Closes #576
1 parent 323a9bb commit 0366a4b

4 files changed

Lines changed: 130 additions & 0 deletions

File tree

openwisp_monitoring/device/apps.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,31 @@ def ready(self):
4444
self.connect_config_status_changed()
4545
self.connect_wifi_client_signals()
4646
self.connect_offline_device_close_wifisession()
47+
self.connect_check_signals()
4748
self.device_recovery_detection()
4849
self.set_update_config_model()
4950
self.register_dashboard_items()
5051
self.register_menu_groups()
5152
self.add_connection_ignore_notification_reasons()
5253

54+
def connect_check_signals(self):
55+
from django.db.models.signals import post_delete, post_save
56+
from swapper import load_model
57+
58+
Check = load_model('check', 'Check')
59+
DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring')
60+
61+
post_save.connect(
62+
DeviceMonitoring.handle_critical_metric,
63+
sender=Check,
64+
dispatch_uid='check_post_save_receiver',
65+
)
66+
post_delete.connect(
67+
DeviceMonitoring.handle_critical_metric,
68+
sender=Check,
69+
dispatch_uid='check_post_delete_receiver',
70+
)
71+
5372
def connect_device_signals(self):
5473
from .api.views import DeviceMetricView
5574

openwisp_monitoring/device/base/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.core.cache import cache
1010
from django.core.exceptions import ValidationError
1111
from django.db import models
12+
from django.db.models.signals import post_delete
1213
from django.dispatch import receiver
1314
from django.utils.timezone import now
1415
from django.utils.translation import gettext_lazy as _
@@ -21,6 +22,7 @@
2122
from swapper import load_model
2223

2324
from openwisp_controller.config.validators import mac_address_validator
25+
from openwisp_monitoring.device.settings import get_critical_device_metrics
2426
from openwisp_utils.base import TimeStampedEditableModel
2527

2628
from ...db import device_data_query, timeseries_db
@@ -454,6 +456,21 @@ def handle_disabled_organization(cls, organization_id):
454456
status='unknown'
455457
)
456458

459+
@classmethod
460+
def _get_critical_metric_keys(cls):
461+
return [metric['key'] for metric in get_critical_device_metrics()]
462+
463+
@classmethod
464+
def handle_critical_metric(cls, instance, **kwargs):
465+
critical_metrics = cls._get_critical_metric_keys()
466+
if instance.check_type in critical_metrics:
467+
try:
468+
device_monitoring = cls.objects.get(device=instance.content_object)
469+
if not instance.is_active or kwargs.get('signal') == post_delete:
470+
device_monitoring.update_status('unknown')
471+
except cls.DoesNotExist:
472+
pass
473+
457474

458475
class AbstractWifiClient(TimeStampedEditableModel):
459476
id = None
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.db import migrations
2+
from swapper import load_model
3+
4+
5+
def update_critical_device_metric_status(apps, schema_editor):
6+
Check = apps.get_model('check', 'Check')
7+
# We need to load the real concrete model here, because we
8+
# will be calling one of its class methods below
9+
DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring')
10+
critical_metrics_keys = DeviceMonitoring._get_critical_metric_keys()
11+
12+
for check in Check.objects.filter(
13+
is_active=False, check_type__in=critical_metrics_keys
14+
).iterator():
15+
DeviceMonitoring.handle_critical_metric(check)
16+
17+
18+
class Migration(migrations.Migration):
19+
dependencies = [
20+
('device_monitoring', '0008_alter_wificlient_options'),
21+
]
22+
23+
operations = [
24+
migrations.RunPython(
25+
update_critical_device_metric_status, reverse_code=migrations.RunPython.noop
26+
),
27+
]

openwisp_monitoring/device/tests/test_models.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,73 @@ def _create_env(self):
578578
)
579579
return dm, ping, load, process_count
580580

581+
def test_disabling_critical_check(self):
582+
Check = load_model('check', 'Check')
583+
dm, ping, load, process_count = self._create_env()
584+
ping_check_instance = Check.objects.create(
585+
name='Ping Check',
586+
check_type='ping',
587+
content_object=dm.device,
588+
params={},
589+
)
590+
dm.update_status('ok')
591+
with catch_signal(health_status_changed) as handler:
592+
ping_check_instance.is_active = False
593+
ping_check_instance.save()
594+
self.assertEqual(handler.call_count, 1)
595+
call_args = handler.call_args[1]
596+
self.assertEqual(call_args['instance'], dm)
597+
self.assertEqual(call_args['status'], 'unknown')
598+
dm.refresh_from_db()
599+
self.assertEqual(dm.status, 'unknown')
600+
with self.subTest(
601+
'Ensure status does not change on saving active critical check'
602+
):
603+
ping_check_instance.is_active = True
604+
ping_check_instance.save()
605+
dm.refresh_from_db()
606+
self.assertEqual(dm.status, 'unknown')
607+
608+
def test_saving_non_critical_check(self):
609+
Check = load_model('check', 'Check')
610+
dm, ping, load, process_count = self._create_env()
611+
# Ensure initial status is 'ok'
612+
dm.update_status('ok')
613+
# Created a non-critical check
614+
non_critical_check = Check.objects.create(
615+
name='Configuration Applied',
616+
check_type='non_critical',
617+
content_object=dm.device,
618+
params={},
619+
)
620+
with catch_signal(health_status_changed) as handler:
621+
non_critical_check.is_active = False
622+
non_critical_check.save()
623+
self.assertEqual(
624+
handler.call_count, 0, 'Signal should not be fired for non-critical check'
625+
)
626+
dm.refresh_from_db()
627+
self.assertEqual(dm.status, 'ok')
628+
629+
def test_deleting_critical_check(self):
630+
Check = load_model('check', 'Check')
631+
dm, ping, load, process_m = self._create_env()
632+
ping_check_instance = Check.objects.create(
633+
name='Ping Check',
634+
check_type='ping',
635+
content_object=dm.device,
636+
params={},
637+
)
638+
dm.update_status('ok')
639+
with catch_signal(health_status_changed) as handler:
640+
ping_check_instance.delete()
641+
self.assertEqual(handler.call_count, 1)
642+
call_args = handler.call_args[1]
643+
self.assertEqual(call_args['instance'], dm)
644+
self.assertEqual(call_args['status'], 'unknown')
645+
dm.refresh_from_db()
646+
self.assertEqual(dm.status, 'unknown')
647+
581648
def test_status_changed(self):
582649
dm, ping, load, process_count = self._create_env()
583650
# check signal

0 commit comments

Comments
 (0)