Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Added support for device deactivation #560 #607

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
pip install -r requirements-test.txt
pip install -U -I -e .
pip uninstall -y django
pip install -U --force-reinstall --no-deps https://github.com/openwisp/openwisp-utils/tarball/extendable-submit-line
pip install -U --force-reinstall --no-deps https://github.com/openwisp/openwisp-controller/tarball/issues/625-device-deactivation
pip install ${{ matrix.django-version }}
sudo npm install -g jshint stylelint

Expand Down
3 changes: 3 additions & 0 deletions openwisp_monitoring/check/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ def check_instance(self):
def perform_check(self, store=True):
"""Initializes check instance and calls the check method."""
if (
hasattr(self.content_object, 'is_deactivated')
and self.content_object.is_deactivated()
) or (
hasattr(self.content_object, 'organization_id')
and self.content_object.organization.is_active is False
):
Expand Down
12 changes: 11 additions & 1 deletion openwisp_monitoring/check/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def test_device_deleted(self):
self.assertEqual(Check.objects.count(), 0)
d = self._create_device(organization=self._create_org())
self.assertEqual(Check.objects.count(), 3)
d.delete()
d.delete(check_deactivated=False)
self.assertEqual(Check.objects.count(), 0)

def test_config_modified_device_problem(self):
Expand Down Expand Up @@ -290,6 +290,16 @@ def test_device_organization_disabled_check_not_performed(self):
check.perform_check()
mocked_check.assert_not_called()

def test_deactivated_device_check_not_performed(self):
config = self._create_config(status='modified', organization=self._create_org())
self.assertEqual(Check.objects.filter(is_active=True).count(), 3)
config.device.deactivate()
config.set_status_deactivated()
check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first()
with patch(f'{self._CONFIG_APPLIED}.check') as mocked_check:
check.perform_check()
mocked_check.assert_not_called()

def test_config_check_problem_with_interval(self):
self._create_admin()
d = self._create_device(organization=self._create_org())
Expand Down
61 changes: 51 additions & 10 deletions openwisp_monitoring/device/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from swapper import load_model

from openwisp_controller.config.admin import DeactivatedDeviceReadOnlyMixin
from openwisp_controller.config.admin import DeviceAdmin as BaseDeviceAdmin
from openwisp_users.multitenancy import MultitenantAdminMixin
from openwisp_utils.admin import ReadOnlyAdmin
Expand Down Expand Up @@ -58,30 +59,68 @@ def full_clean(self):


class InlinePermissionMixin:
"""
Manages permissions for the inline objects.

This mixin class handles the permissions for the inline objects.
It checks if the user has permission to modify the main object
(e.g., view_model, add_model, change_model, or delete_model),
and if not, it checks if the user has permission to modify the
inline object (e.g., view_model_inline, add_model_inline,
change_model_inline, or delete_model_inline).
"""

def has_add_permission(self, request, obj=None):
# User will be able to add objects from inline even
# if it only has permission to add a model object
return super().has_add_permission(request, obj) or request.user.has_perm(
return super(admin.options.InlineModelAdmin, self).has_add_permission(
request
) or request.user.has_perm(
f'{self.model._meta.app_label}.add_{self.inline_permission_suffix}'
)

def has_change_permission(self, request, obj=None):
return super().has_change_permission(request, obj) or request.user.has_perm(
return super(admin.options.InlineModelAdmin, self).has_change_permission(
request, obj
) or request.user.has_perm(
f'{self.model._meta.app_label}.change_{self.inline_permission_suffix}'
)

def has_view_permission(self, request, obj=None):
return super().has_view_permission(request, obj) or request.user.has_perm(
return super(admin.options.InlineModelAdmin, self).has_view_permission(
request, obj
) or request.user.has_perm(
f'{self.model._meta.app_label}.view_{self.inline_permission_suffix}'
)

def has_delete_permission(self, request, obj=None):
return super().has_delete_permission(request, obj) or request.user.has_perm(
return super(admin.options.InlineModelAdmin, self).has_delete_permission(
request, obj
) or request.user.has_perm(
f'{self.model._meta.app_label}.delete_{self.inline_permission_suffix}'
)


class CheckInline(InlinePermissionMixin, GenericStackedInline):
class DeactivatedDeviceReadOnlyInlinePermissionMixin(
DeactivatedDeviceReadOnlyMixin,
InlinePermissionMixin,
):
def has_add_permission(self, request, obj=None):
perm = super().has_add_permission(request, obj)
return self._has_permission(request, obj, perm)

def has_change_permission(self, request, obj=None):
perm = super().has_change_permission(request, obj)
return self._has_permission(request, obj, perm)

def has_view_permission(self, request, obj=None):
perm = super().has_view_permission(request, obj)
return self._has_permission(request, obj, perm)

def has_delete_permission(self, request, obj=None):
perm = super().has_delete_permission(request, obj)
return self._has_permission(request, obj, perm)


class CheckInline(DeactivatedDeviceReadOnlyInlinePermissionMixin, GenericStackedInline):
model = Check
extra = 0
formset = CheckInlineFormSet
Expand Down Expand Up @@ -152,7 +191,9 @@ def get_queryset(self, request):
return super().get_queryset(request).order_by('created')


class MetricInline(InlinePermissionMixin, NestedGenericStackedInline):
class MetricInline(
DeactivatedDeviceReadOnlyInlinePermissionMixin, NestedGenericStackedInline
):
model = Metric
extra = 0
inlines = [AlertSettingsInline]
Expand Down Expand Up @@ -205,7 +246,7 @@ def has_delete_permission(self, request, obj=None):


class DeviceAdmin(BaseDeviceAdmin, NestedModelAdmin):
change_form_template = 'admin/config/device/change_form.html'
change_form_template = 'admin/monitoring/device/change_form.html'
list_filter = ['monitoring__status'] + BaseDeviceAdmin.list_filter
list_select_related = ['monitoring'] + list(BaseDeviceAdmin.list_select_related)
list_display = list(BaseDeviceAdmin.list_display)
Expand Down Expand Up @@ -415,7 +456,7 @@ class WiFiSessionInline(WifiSessionAdminHelperMixin, admin.TabularInline):
readonly_fields = fields
can_delete = False
extra = 0
template = 'admin/config/device/wifisession_tabular.html'
template = 'admin/monitoring/device/wifisession_tabular.html'

class Media:
css = {'all': ('monitoring/css/wifi-sessions.css',)}
Expand Down
2 changes: 1 addition & 1 deletion openwisp_monitoring/device/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class DeviceMetricView(

model = DeviceData
queryset = (
DeviceData.objects.filter(organization__is_active=True)
DeviceData.objects.filter(organization__is_active=True, _is_deactivated=False)
.only(
'id',
'key',
Expand Down
12 changes: 11 additions & 1 deletion openwisp_monitoring/device/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from django.utils.translation import gettext_lazy as _
from swapper import get_model_name, load_model

from openwisp_controller.config.signals import checksum_requested, config_status_changed
from openwisp_controller.config.signals import (
checksum_requested,
config_status_changed,
device_deactivated,
)
from openwisp_controller.connection import settings as connection_settings
from openwisp_controller.connection.signals import is_working_changed
from openwisp_utils.admin_theme import (
Expand Down Expand Up @@ -74,6 +78,7 @@ def connect_device_signals(self):

Device = load_model('config', 'Device')
DeviceData = load_model('device_monitoring', 'DeviceData')
DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring')
DeviceLocation = load_model('geo', 'DeviceLocation')
Metric = load_model('monitoring', 'Metric')
Chart = load_model('monitoring', 'Chart')
Expand Down Expand Up @@ -145,6 +150,11 @@ def connect_device_signals(self):
sender=Organization,
dispatch_uid='post_save_organization_disabled_monitoring',
)
device_deactivated.connect(
DeviceMonitoring.handle_deactivated_device,
sender=Device,
dispatch_uid='device_deactivated_update_devicemonitoring',
)

@classmethod
def device_post_save_receiver(cls, instance, created, **kwargs):
Expand Down
13 changes: 13 additions & 0 deletions openwisp_monitoring/device/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,19 @@ def handle_disabled_organization(cls, organization_id):
status='unknown'
)

@classmethod
def handle_deactivated_device(cls, instance, **kwargs):
"""Handles the deactivation of a device

Sets the device's monitoring status to 'deactivated'

Parameters: - instance (Device): The device object
which is deactivated

Returns: - None
"""
cls.objects.filter(device_id=instance.id).update(status='deactivated')

@classmethod
def _get_critical_metric_keys(cls):
return [metric['key'] for metric in get_critical_device_metrics()]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.health-unknown {
.health-unknown,
.health-deactivated {
color: grey;
}
.health-ok {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ td.field-health_status,
font-weight: bold;
text-transform: uppercase;
}
.health-unknown {
.health-unknown,
.health-deactivated {
color: grey;
}
.health-ok {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "admin/config/change_form.html" %}
{% extends "admin/config/device/change_form.html" %}
{% load i18n static %}
{% block after_field_sets %}
{% if not add and device_data %}
Expand Down
6 changes: 4 additions & 2 deletions openwisp_monitoring/device/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,11 +572,11 @@ def test_check_alertsetting_inline(self):
self.client.force_login(test_user)

def _add_device_permissions(user):
test_user.user_permissions.clear()
user.user_permissions.clear()
self.assertEqual(user.user_permissions.count(), 0)
device_permissions = Permission.objects.filter(codename__endswith='device')
# Permissions required to access device page
test_user.user_permissions.add(*device_permissions),
user.user_permissions.add(*device_permissions),
self.assertEqual(user.user_permissions.count(), 4)

def _add_user_permissions(user, permission_query, expected_perm_count):
Expand Down Expand Up @@ -989,6 +989,8 @@ def test_wifi_session_chart_on_index(self):

def test_deleting_device_with_wifisessions(self):
device_data = self._save_device_data()
device = Device.objects.first()
device.deactivate()
path = reverse('admin:config_device_delete', args=[device_data.pk])
response = self.client.post(path, {'post': 'yes'}, follow=True)
self.assertEqual(response.status_code, 200)
Expand Down
11 changes: 10 additions & 1 deletion openwisp_monitoring/device/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def test_200_none(self):
# Add 1 for general metric and chart
self.assertEqual(self.metric_queryset.count(), 0)
self.assertEqual(self.chart_queryset.count(), 0)
d.delete()
d.delete(check_deactivated=False)
r = self._post_data(d.id, d.key, data)
self.assertEqual(r.status_code, 404)

Expand Down Expand Up @@ -348,6 +348,15 @@ def test_404_disabled_organization(self):
self.assertEqual(self.metric_queryset.count(), 0)
self.assertEqual(self.chart_queryset.count(), 0)

def test_404_deactivated_device(self):
device = self._create_device()
device.deactivate()
with self.assertNumQueries(2):
response = self._post_data(device.id, device.key, self._data())
self.assertEqual(response.status_code, 404)
self.assertEqual(self.metric_queryset.count(), 0)
self.assertEqual(self.chart_queryset.count(), 0)

def test_garbage_wireless_clients(self):
o = self._create_org()
d = self._create_device(organization=o)
Expand Down
13 changes: 11 additions & 2 deletions openwisp_monitoring/device/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def test_device_deleted(self):
metric = self._create_object_metric(name='test', content_object=d)
metric.full_clean()
metric.save()
d.delete()
d.delete(check_deactivated=False)
try:
metric.refresh_from_db()
except ObjectDoesNotExist:
Expand Down Expand Up @@ -777,7 +777,7 @@ def test_deleting_device_deletes_tsdb(self):
ping2.write(0)
self.assertNotEqual(self._read_metric(ping1), [])
self.assertNotEqual(self._read_metric(ping2), [])
dm1.device.delete()
dm1.device.delete(check_deactivated=False)
# Only the metric related to the deleted device
# is deleted
self.assertEqual(self._read_metric(ping1), [])
Expand All @@ -797,6 +797,15 @@ def test_handle_disabled_organization(self):
self.assertEqual(device_monitoring.status, 'unknown')
self.assertEqual(device.management_ip, None)

def test_handle_deactivated_device(self):
device_monitoring, _, _, _ = self._create_env()
device = device_monitoring.device
self.assertEqual(device_monitoring.status, 'ok')
device.deactivate()
device_monitoring.refresh_from_db()
device.refresh_from_db()
self.assertEqual(device_monitoring.status, 'deactivated')


class TestWifiClientSession(TestWifiClientSessionMixin, TestCase):
wifi_client_model = WifiClient
Expand Down
2 changes: 2 additions & 0 deletions openwisp_monitoring/tests/test_selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def test_restoring_deleted_device(self):
self.fail('Failed saving device')

# Delete the device
device.deactivate()
device.config.set_status_deactivated()
self.open(
reverse(f'admin:{self.config_app_label}_device_delete', args=[device.id])
)
Expand Down
Loading