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

Add webhook for UISP device deactivation #781

Merged
merged 2 commits into from
Dec 24, 2024
Merged
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
11 changes: 11 additions & 0 deletions src/meshapi/util/uisp_import/update_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import List, Optional

import requests
from drf_hooks.signals import hook_event

from meshapi.models import Device, Link, Node
from meshapi.util.uisp_import.constants import (
Expand Down Expand Up @@ -31,6 +32,7 @@ def update_device_from_uisp_data(
)
existing_device.node = uisp_node

fire_device_deactivated_hook = False
if existing_device.status != uisp_status:
if uisp_status == Device.DeviceStatus.INACTIVE:
# We wait 30 days to make sure this device is actually inactive,
Expand All @@ -46,6 +48,7 @@ def update_device_from_uisp_data(
" for more than "
f"{int(UISP_OFFLINE_DURATION_BEFORE_MARKING_INACTIVE.total_seconds() / 60 / 60 / 24)} days"
)
fire_device_deactivated_hook = True

existing_device.status = Device.DeviceStatus.INACTIVE
change_messages.append(change_message)
Expand Down Expand Up @@ -78,6 +81,14 @@ def update_device_from_uisp_data(
change_messages.append(f"Added missing abandon date of {existing_device.abandon_date} based on UISP last-seen")

existing_device.save()

if fire_device_deactivated_hook:
hook_event.send(
sender=existing_device.__class__,
action="uisp-deactivated",
instance=existing_device,
)

return change_messages


Expand Down
65 changes: 57 additions & 8 deletions src/meshapi_hooks/tests/test_webhooks.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import multiprocessing
import queue
from datetime import timezone

from django.contrib.auth.models import Permission, User
from django.utils.datetime_safe import datetime
from flask import Flask, Response, request

from meshapi.util.uisp_import.update_objects import update_device_from_uisp_data
from meshapi_hooks.hooks import CelerySerializerHook

multiprocessing.set_start_method("fork")

from celery.contrib.testing.worker import start_worker
from django.test import TransactionTestCase

from meshapi.models import Building, Install, Member
from meshapi.tests.sample_data import sample_building, sample_install, sample_member
from meshapi.models import Building, Device, Install, Member, Node
from meshapi.tests.sample_data import sample_building, sample_device, sample_install, sample_member, sample_node
from meshdb.celery import app as celery_app

HTTP_CALL_WAITING_TIME = 2 # Seconds
Expand Down Expand Up @@ -78,33 +81,34 @@ def setUp(self):
)
self.app_process.start()

hook_user = User.objects.create_user(
self.hook_user = User.objects.create_user(
username="hook_client_application", password="test_pw", email="[email protected]"
)
hook_user.user_permissions.add(Permission.objects.get(codename="view_member"))
hook_user.user_permissions.add(Permission.objects.get(codename="view_install"))
self.hook_user.user_permissions.add(Permission.objects.get(codename="view_member"))
self.hook_user.user_permissions.add(Permission.objects.get(codename="view_install"))
self.hook_user.user_permissions.add(Permission.objects.get(codename="view_device"))

# For testing, just so that we don't have to wait around for a large number of failures
CelerySerializerHook.MAX_CONSECUTIVE_FAILURES_BEFORE_DISABLE = 1

# Create the webhooks in Django
# (this would be done by an admin via the UI in prod)
member_webhook = CelerySerializerHook(
user=hook_user,
user=self.hook_user,
target="http://localhost:8091/webhook",
event="member.created",
)
member_webhook.save()
install_webhook = CelerySerializerHook(
user=hook_user,
user=self.hook_user,
target="http://localhost:8091/webhook",
event="install.created",
)
install_webhook.save()

building_webhook = CelerySerializerHook(
enabled=False,
user=hook_user,
user=self.hook_user,
target="http://localhost:8091/webhook",
event="install.created",
)
Expand Down Expand Up @@ -245,3 +249,48 @@ def test_building(self):
assert False, "HTTP server shouldn't have been called"
except queue.Empty:
pass

def test_uisp_update_event(self):
# UISP update triggers custom webhook
device_webhook = CelerySerializerHook(
enabled=True,
user=self.hook_user,
target="http://localhost:8091/webhook",
event="device.uisp-deactivated",
)
device_webhook.save()

uisp_node = Node(**sample_node)
uisp_node.save()

existing_device = Device(**sample_device)
existing_device.node = uisp_node
existing_device.save()

uisp_name = "test"
uisp_status = Device.DeviceStatus.INACTIVE

uisp_last_seen = datetime(1970, 1, 1, 1, 1, 1, tzinfo=timezone.utc)

update_device_from_uisp_data(
existing_device,
uisp_node,
uisp_name,
uisp_status,
uisp_last_seen,
)

try:
flask_request = self.http_requests_queue.get(timeout=HTTP_CALL_WAITING_TIME)
except queue.Empty as e:
raise RuntimeError("HTTP server not called...") from e

assert flask_request["data"]["id"] == str(existing_device.id)
assert flask_request["data"]["status"] == "Inactive"
assert flask_request["data"]["abandon_date"] == "1970-01-01"
assert flask_request["data"]["node"]["id"] == str(uisp_node.id)
assert flask_request["data"]["node"]["network_number"] == uisp_node.network_number

assert flask_request["hook"]["event"] == "device.uisp-deactivated"
assert flask_request["hook"]["target"] == "http://localhost:8091/webhook"
assert flask_request["hook"]["id"]
1 change: 1 addition & 0 deletions src/meshdb/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@
"link.updated": "meshapi.Link.updated+",
"los.updated": "meshapi.LOS.updated+",
"device.updated": "meshapi.Device.updated+",
"device.uisp-deactivated": "meshapi.Device.uisp-deactivated+",
"sector.updated": "meshapi.Sector.updated+",
"access_point.updated": "meshapi.AccessPoint.updated+",
"building.deleted": "meshapi.Building.deleted+",
Expand Down
Loading