Skip to content

Commit 0094d9e

Browse files
committed
[feature] Add a check which inspects device configuration status periodically #54
1 parent 4b849dd commit 0094d9e

File tree

21 files changed

+455
-89
lines changed

21 files changed

+455
-89
lines changed

README.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,47 @@ in terms of disk space.
242242

243243
Whether ping checks are created automatically for devices.
244244

245+
``OPENWISP_MONITORING_AUTO_CONFIG_STATUS``
246+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
247+
248+
+--------------+-------------+
249+
| **type**: | ``bool`` |
250+
+--------------+-------------+
251+
| **default**: | ``True`` |
252+
+--------------+-------------+
253+
254+
Whether config_status checks are created automatically for devices.
255+
256+
``OPENWISP_MONITORING_CONFIG_MODIFIED_MAX_TIME``
257+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
258+
259+
+--------------+-----------+
260+
| **type**: | ``float`` |
261+
+--------------+-----------+
262+
| **default**: | ``5`` |
263+
+--------------+-----------+
264+
265+
After ``config`` is ``modified``, if the ``modified`` status does not change after a
266+
fixed **duration** then ``device`` health status changes to ``problem``.
267+
This **duration** can be set with the help of this setting. The input represents the duration in minutes.
268+
Thus, by default the health status of the device changes to ``problem`` after 5 minutes of ``config_modified`` status.
269+
270+
**Note**: If the setting ``AUTO_CONFIG_STATUS`` is disabled then this setting need not be declared.
271+
272+
``OPENWISP_MONITORING_CONFIG_STATUS_RETENTION_POLICY``
273+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
274+
275+
+--------------+-------------+
276+
| **type**: | ``time`` |
277+
+--------------+-------------+
278+
| **default**: | ``48h0m0s`` |
279+
+--------------+-------------+
280+
281+
This setting allows to modify the duration for which the metric data generated
282+
by ``modified_status`` check is to be retained.
283+
284+
**Note**: If the setting ``AUTO_CONFIG_STATUS`` is disabled then this setting need not be declared.
285+
245286
``OPENWISP_MONITORING_AUTO_GRAPHS``
246287
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
247288

openwisp_monitoring/check/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from django.apps import AppConfig
22
from django.utils.translation import ugettext_lazy as _
33

4+
from .utils import manage_config_status_retention_policy
5+
46

57
class CheckConfig(AppConfig):
68
name = 'openwisp_monitoring.check'
79
label = 'check'
810
verbose_name = _('Network Monitoring Checks')
11+
12+
def ready(self):
13+
manage_config_status_retention_policy()

openwisp_monitoring/check/base/models.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from django.contrib.contenttypes.fields import GenericForeignKey
44
from django.contrib.contenttypes.models import ContentType
5-
from django.db import models
5+
from django.db import models, transaction
66
from django.db.models.signals import post_save
7+
from django.dispatch import receiver
78
from django.utils.functional import cached_property
89
from django.utils.module_loading import import_string
910
from django.utils.translation import ugettext_lazy as _
@@ -75,27 +76,45 @@ def perform_check(self, store=True):
7576
return self.check_instance.check(store=True)
7677

7778

78-
if app_settings.AUTO_PING:
79-
from django.db import transaction
80-
from django.dispatch import receiver
79+
@receiver(post_save, sender=Device, dispatch_uid='auto_ping')
80+
def auto_ping_receiver(sender, instance, created, **kwargs):
81+
"""
82+
Implements OPENWISP_MONITORING_AUTO_PING
83+
The creation step is executed in the background
84+
"""
85+
# we need to skip this otherwise this task will be executed
86+
# every time the configuration is requested via checksum
8187
from openwisp_monitoring.check.tasks import auto_create_ping
8288

83-
@receiver(post_save, sender=Device, dispatch_uid='auto_ping')
84-
def auto_ping_receiver(sender, instance, created, **kwargs):
85-
"""
86-
Implements OPENWISP_MONITORING_AUTO_PING
87-
The creation step is executed in the backround
88-
"""
89-
# we need to skip this otherwise this task will be executed
90-
# every time the configuration is requested via checksum
91-
if not created:
92-
return
93-
with transaction.atomic():
94-
transaction.on_commit(
95-
lambda: auto_create_ping.delay(
96-
model=sender.__name__.lower(),
97-
app_label=sender._meta.app_label,
98-
object_id=str(instance.pk),
99-
created=created,
100-
)
89+
if not app_settings.AUTO_PING or not created:
90+
return
91+
with transaction.atomic():
92+
transaction.on_commit(
93+
lambda: auto_create_ping.delay(
94+
model=sender.__name__.lower(),
95+
app_label=sender._meta.app_label,
96+
object_id=str(instance.pk),
97+
)
98+
)
99+
100+
101+
@receiver(post_save, sender=Device, dispatch_uid='auto_config_status')
102+
def auto_config_status_receiver(sender, instance, created, **kwargs):
103+
"""
104+
Implements OPENWISP_MONITORING_AUTO_CONFIG_STATUS
105+
The creation step is executed in the background
106+
"""
107+
# we need to skip this otherwise this task will be executed
108+
# every time the configuration is requested via checksum
109+
from openwisp_monitoring.check.tasks import auto_create_config_status
110+
111+
if not app_settings.AUTO_CONFIG_STATUS or not created:
112+
return
113+
with transaction.atomic():
114+
transaction.on_commit(
115+
lambda: auto_create_config_status.delay(
116+
model=sender.__name__.lower(),
117+
app_label=sender._meta.app_label,
118+
object_id=str(instance.pk),
101119
)
120+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from .config_status import ConfigStatus # noqa
12
from .ping import Ping # noqa
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django.contrib.contenttypes.models import ContentType
2+
from django.core.exceptions import ValidationError
3+
from swapper import load_model
4+
5+
from openwisp_controller.config.models import Device
6+
7+
Metric = load_model('monitoring', 'Metric')
8+
9+
10+
class BaseCheck(object):
11+
def validate_instance(self):
12+
# check instance is of type device
13+
obj = self.related_object
14+
if not obj or not isinstance(obj, Device):
15+
message = 'A related device is required to perform this operation'
16+
raise ValidationError({'content_type': message, 'object_id': message})
17+
18+
def _get_or_create_metric(self, field_name):
19+
"""
20+
Gets or creates metric
21+
"""
22+
check = self.check_instance
23+
if check.object_id and check.content_type:
24+
obj_id = check.object_id
25+
ct = check.content_type
26+
else:
27+
obj_id = str(check.id)
28+
ct = ContentType.objects.get(
29+
app_label=check._meta.app_label, model=check.__class__.__name__.lower()
30+
)
31+
options = dict(
32+
name=check.name,
33+
object_id=obj_id,
34+
content_type=ct,
35+
field_name=field_name,
36+
key=self.__class__.__name__.lower(),
37+
)
38+
metric, created = Metric.objects.get_or_create(**options)
39+
return metric, created
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from time import time
2+
3+
from swapper import load_model
4+
5+
from ...monitoring.utils import write
6+
from ..settings import CONFIG_MODIFIED_MAX_TIME
7+
from ..utils import CONFIG_STATUS_RP
8+
from .base import BaseCheck
9+
10+
Graph = load_model('monitoring', 'Graph')
11+
Threshold = load_model('monitoring', 'Threshold')
12+
13+
14+
class ConfigStatus(BaseCheck):
15+
def __init__(self, check, params):
16+
self.check_instance = check
17+
self.related_object = check.content_object
18+
self.params = params
19+
20+
def validate(self):
21+
self.validate_instance()
22+
23+
def check(self, store=True):
24+
if not hasattr(self.related_object, 'config'):
25+
return
26+
result = 0 if self.related_object.config.status == 'applied' else 1
27+
if result and self._check_modified_status_crossed_max_time():
28+
# TODO: When is the status supposed to be made critical?
29+
dm = self.related_object.monitoring
30+
if dm.status in ['ok', 'unknown']:
31+
dm.update_status('problem')
32+
if store:
33+
metric = self.get_metric()
34+
# TODO: Find why doing the same by adding rp as an arg
35+
# in metric write changes status to problem
36+
write(
37+
name=metric.key,
38+
values={metric.field_name: result},
39+
tags=metric.tags,
40+
retention_policy=CONFIG_STATUS_RP,
41+
)
42+
return result
43+
44+
# TODO: Check if this can be done using threshold
45+
def _check_modified_status_crossed_max_time(self):
46+
# A small margin is kept to take care of border cases
47+
# TODO: Find why `m` --> minute is not working correctly!
48+
since = f'now() - {int(CONFIG_MODIFIED_MAX_TIME*63)}s'
49+
measurement_list = self.get_metric().read(
50+
since=since, limit=None, order='time DESC'
51+
)
52+
for measurement in measurement_list:
53+
if not measurement['config_status']:
54+
break
55+
minutes_difference = (time() - measurement['time']) / 60
56+
if minutes_difference >= CONFIG_MODIFIED_MAX_TIME:
57+
return True
58+
return False
59+
60+
def get_metric(self):
61+
metric, created = self._get_or_create_metric(field_name='config_status')
62+
if created:
63+
self._create_threshold(metric)
64+
return metric
65+
66+
def _create_threshold(self, metric):
67+
t = Threshold(metric=metric, operator='>', value=0, seconds=0)
68+
t.full_clean()
69+
t.save()

openwisp_monitoring/check/classes/ping.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
import subprocess
22

3-
from django.contrib.contenttypes.models import ContentType
43
from django.core.exceptions import ValidationError
54
from jsonschema import draft7_format_checker, validate
65
from jsonschema.exceptions import ValidationError as SchemaError
76
from swapper import load_model
87

9-
from openwisp_controller.config.models import Device
10-
118
from ... import settings as monitoring_settings
129
from .. import settings as app_settings
1310
from ..exceptions import OperationalError
11+
from .base import BaseCheck
1412

1513
Graph = load_model('monitoring', 'Graph')
16-
Metric = load_model('monitoring', 'Metric')
1714
Threshold = load_model('monitoring', 'Threshold')
1815

1916

20-
class Ping(object):
17+
class Ping(BaseCheck):
2118
schema = {
22-
'$schema': 'http://json-schema.org/draft-04/schema#',
19+
'$schema': 'http://json-schema.org/draft-07/schema#',
2320
'type': 'object',
2421
'additionalProperties': False,
2522
'properties': {
@@ -57,13 +54,6 @@ def validate(self):
5754
self.validate_instance()
5855
self.validate_params()
5956

60-
def validate_instance(self):
61-
# check instance is of type device
62-
obj = self.related_object
63-
if not obj or not isinstance(obj, Device):
64-
message = 'A related device is required ' 'to perform this operation'
65-
raise ValidationError({'content_type': message, 'object_id': message})
66-
6757
def validate_params(self):
6858
try:
6959
validate(self.params, self.schema, format_checker=draft7_format_checker)
@@ -165,23 +155,7 @@ def _get_metric(self):
165155
"""
166156
Gets or creates metric
167157
"""
168-
check = self.check_instance
169-
if check.object_id and check.content_type:
170-
obj_id = check.object_id
171-
ct = check.content_type
172-
else:
173-
obj_id = str(check.id)
174-
ct = ContentType.objects.get(
175-
app_label=check._meta.app_label, model=check.__class__.__name__.lower()
176-
)
177-
options = dict(
178-
name=check.name,
179-
object_id=obj_id,
180-
content_type=ct,
181-
field_name='reachable',
182-
key=self.__class__.__name__.lower(),
183-
)
184-
metric, created = Metric.objects.get_or_create(**options)
158+
metric, created = self._get_or_create_metric(field_name='reachable')
185159
if created:
186160
self._create_threshold(metric)
187161
self._create_graphs(metric)

openwisp_monitoring/check/migrations/0001_initial.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import collections
1010
import swapper
1111

12+
from ..settings import CHECK_CLASSES
13+
1214

1315
class Migration(migrations.Migration):
1416

@@ -58,7 +60,7 @@ class Migration(migrations.Migration):
5860
(
5961
'check',
6062
models.CharField(
61-
choices=[('openwisp_monitoring.check.classes.Ping', 'Ping')],
63+
choices=CHECK_CLASSES,
6264
db_index=True,
6365
help_text='Select check type',
6466
max_length=128,

openwisp_monitoring/check/migrations/0003_create_ping.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ def create_device_ping(apps, schema_editor):
1111
model=Device.__name__.lower(),
1212
app_label=Device._meta.app_label,
1313
object_id=str(device.pk),
14-
created=True,
1514
)
1615

1716

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from django.db import migrations
2+
from openwisp_monitoring.check.settings import AUTO_CONFIG_STATUS
3+
from openwisp_monitoring.check.tasks import auto_create_config_status
4+
5+
6+
def add_config_status_checks(apps, schema_editor):
7+
if not AUTO_CONFIG_STATUS:
8+
return
9+
Device = apps.get_model('config', 'Device')
10+
for device in Device.objects.all():
11+
auto_create_config_status.delay(
12+
model=Device.__name__.lower(),
13+
app_label=Device._meta.app_label,
14+
object_id=str(device.pk),
15+
)
16+
17+
18+
def remove_config_status_checks(apps, schema_editor):
19+
Check = apps.get_model('config', 'Device')
20+
Check.objects.filter(name='Config Status').delete()
21+
22+
23+
class Migration(migrations.Migration):
24+
25+
dependencies = [
26+
('check', '0003_create_ping'),
27+
]
28+
29+
operations = [
30+
migrations.RunPython(
31+
add_config_status_checks, reverse_code=remove_config_status_checks
32+
),
33+
]

0 commit comments

Comments
 (0)