From c4e60bd05fa0de367ebe9df8978194ce1a54a3c4 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Fri, 31 Jan 2025 18:02:11 +0000
Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20New=20Webhook=20payload=20table?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/api/serializers.py          | 10 +++++-
 netbox_sys_plugin/forms/createvm.py           | 12 +++++--
 .../migrations/0006_webhookpayload.py         | 33 +++++++++++++++++++
 netbox_sys_plugin/models/operation.py         | 21 ++++++++++++
 4 files changed, 72 insertions(+), 4 deletions(-)
 create mode 100644 netbox_sys_plugin/migrations/0006_webhookpayload.py

diff --git a/netbox_sys_plugin/api/serializers.py b/netbox_sys_plugin/api/serializers.py
index 35af0e1..7f746dd 100644
--- a/netbox_sys_plugin/api/serializers.py
+++ b/netbox_sys_plugin/api/serializers.py
@@ -7,7 +7,7 @@ from dcim.api.nested_serializers import (
 from ipam.api.nested_serializers import (
     NestedIPAddressSerializer, NestedServiceSerializer
 )
-from .. models import VirtualMachineMaintenance, ProviderCredentials,VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings,ProviderTypeExtraConfig,VmAssignedExtraConfig
+from .. models import VirtualMachineMaintenance, ProviderCredentials,VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings,ProviderTypeExtraConfig,VmAssignedExtraConfig, WebhookPayload
 from django.contrib.contenttypes.models import ContentType
 from .nested_serializers import * 
 from virtualization.choices import *
@@ -212,6 +212,14 @@ class WebhookSettingsSerializer(NetBoxModelSerializer):
             raise serializers.ValidationError("You can only have one webbook setting.")
         return data
 
+class WebhookPayloadSerializer(NetBoxModelSerializer):
+    id = serializers.IntegerField(read_only=True)
+    payload_data = serializers.JSONField()
+
+    class Meta:
+        model = WebhookPayload
+        fields = '__all__'
+
 class ProviderTypeExtraConfigSerializer(NetBoxModelSerializer):
     id = serializers.IntegerField(read_only=True)
     extra_config_structure = serializers.JSONField()
diff --git a/netbox_sys_plugin/forms/createvm.py b/netbox_sys_plugin/forms/createvm.py
index 4b35f05..826743d 100644
--- a/netbox_sys_plugin/forms/createvm.py
+++ b/netbox_sys_plugin/forms/createvm.py
@@ -13,8 +13,8 @@ from virtualization.models import ClusterType, Cluster, VirtualMachine, VMInterf
 from ipam.models import IPAddress, Service
 from dcim.models import DeviceRole, Platform
 from extras.models import Tag, TaggedItem
-from .. models import DomainNames, VmAssignedVirtualMachineType, VirtualMachineType, VmAssignedExtraConfig, ProviderTypeExtraConfig
-from . . utils import send_webhook, validate_extra_config_values
+from .. models import DomainNames, VmAssignedVirtualMachineType, VirtualMachineType, VmAssignedExtraConfig, ProviderTypeExtraConfig, WebhookPayload
+from . . utils import validate_extra_config_values
 
 
 
@@ -643,8 +643,14 @@ class CreateVmForm(NetBoxModelForm):
                 ],
                     }
 
+                wb_payload = WebhookPayload(
+                    payload_data=payload
+                )
+                wb_payload.full_clean()
+                wb_payload.save()
+                CreateVmForm.assign_tags(wb_payload,data)
                 # Send webhook
-                send_webhook(payload)
+                #send_webhook(payload)
                 return vm
 
         except ValueError as e:
diff --git a/netbox_sys_plugin/migrations/0006_webhookpayload.py b/netbox_sys_plugin/migrations/0006_webhookpayload.py
new file mode 100644
index 0000000..28b2fc7
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0006_webhookpayload.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.16 on 2025-01-31 17:35
+
+from django.db import migrations, models
+import taggit.managers
+import utilities.json
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0098_webhook_custom_field_data_webhook_tags'),
+        ('netbox_sys_plugin', '0005_alter_webhooksettings_payload_url'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='WebhookPayload',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('created', models.DateTimeField(auto_now_add=True, null=True)),
+                ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+                ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
+                ('payload_data', models.JSONField(blank=True)),
+                ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+            ],
+            options={
+                'verbose_name': 'Webhook Payload',
+                'verbose_name_plural': 'Webhook Payloads',
+                'ordering': ['payload_data'],
+                'unique_together': {('payload_data',)},
+            },
+        ),
+    ]
diff --git a/netbox_sys_plugin/models/operation.py b/netbox_sys_plugin/models/operation.py
index 7d3eb4d..06126b7 100644
--- a/netbox_sys_plugin/models/operation.py
+++ b/netbox_sys_plugin/models/operation.py
@@ -35,3 +35,24 @@ class WebhookSettings(NetBoxModel):
     def get_absolute_url(self):
         """override"""
         return reverse("plugins:netbox_sys_plugin:webhooksettings", args=[self.pk])
+
+class WebhookPayload(NetBoxModel):
+    """Webhook Payload definition class"""
+
+    payload_data = models.JSONField(
+        blank=True,
+        null=False,
+        help_text=_(
+            "JSON Payload to be sent"
+        )
+    )
+    class Meta:
+        """Meta class"""
+        unique_together = ["payload_data"]
+        ordering = ["payload_data"]
+        verbose_name = "Webhook Payload"
+        verbose_name_plural = "Webhook Payloads"
+
+    def get_absolute_url(self):
+        """override"""
+        return reverse("plugins:netbox_sys_plugin:webhookpayloads", args=[self.pk])
-- 
GitLab


From 67ccac2d089e69a3f2e0b1f4f557901ae682bbd7 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Mon, 3 Feb 2025 10:17:23 +0000
Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20=F0=9F=92=84=20Added=20UI=20par?=
 =?UTF-8?q?t=20just=20to=20list=20and=20delete=20rows=20and=20API=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/api/urls.py                 |   1 +
 netbox_sys_plugin/api/views.py                |   7 +-
 netbox_sys_plugin/navigation.py               |   7 +-
 netbox_sys_plugin/tables.py                   |  27 ++++
 .../tests/webhook_payload/__init__.py         |   0
 .../test_webhook_payload_api.py               | 127 ++++++++++++++++++
 netbox_sys_plugin/urls.py                     |   9 ++
 netbox_sys_plugin/views.py                    |  25 ++++
 8 files changed, 201 insertions(+), 2 deletions(-)
 create mode 100644 netbox_sys_plugin/tests/webhook_payload/__init__.py
 create mode 100644 netbox_sys_plugin/tests/webhook_payload/test_webhook_payload_api.py

diff --git a/netbox_sys_plugin/api/urls.py b/netbox_sys_plugin/api/urls.py
index 47e62cc..2766642 100644
--- a/netbox_sys_plugin/api/urls.py
+++ b/netbox_sys_plugin/api/urls.py
@@ -8,6 +8,7 @@ router.register(r'AssignedVmType', VmAssignedVirtualMachineTypeViewSet)
 router.register(r'VmType', VirtualMachineTypeViewSet)
 router.register(r'DomainNames', DomainNamesViewSet)
 router.register(r'WebhookSettings', WebhookSettingsViewSet)
+router.register(r'WebhookPayload', WebhookPayloadViewSet)
 router.register(r'ProviderTypeExtraConfig', ProviderTypeExtraConfigViewSet)
 router.register(r'VirtualMachine', VirtualMachineViewSet, basename='virtualmachine')
 router.register(r'ExtraConfig', ProviderTypeExtraConfigViewSet)
diff --git a/netbox_sys_plugin/api/views.py b/netbox_sys_plugin/api/views.py
index 0e0c294..3d9ed9f 100644
--- a/netbox_sys_plugin/api/views.py
+++ b/netbox_sys_plugin/api/views.py
@@ -5,7 +5,7 @@ from django.db.models import Prefetch
 from netbox.api.viewsets import NetBoxModelViewSet
 from virtualization import filtersets
 from .. filtersets import VmTypeFilterSet, ProviderTypeExtraConfigFilterSet
-from .. models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings, ProviderTypeExtraConfig
+from .. models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings, ProviderTypeExtraConfig, WebhookPayload
 from . serializers import *
 
 class VirtualMachineMaintenanceViewSet(viewsets.ModelViewSet):
@@ -55,6 +55,11 @@ class WebhookSettingsViewSet(viewsets.ModelViewSet):
     serializer_class = WebhookSettingsSerializer
     http_method_names = ["get", "post", "patch", "delete", "options"]
 
+class WebhookPayloadViewSet(viewsets.ModelViewSet):
+    queryset = WebhookPayload.objects.all()
+    serializer_class = WebhookPayloadSerializer
+    http_method_names = ["get", "post", "delete", "options"]
+
 class ProviderTypeExtraConfigViewSet(viewsets.ModelViewSet):
     queryset = ProviderTypeExtraConfig.objects.all()
     serializer_class = ProviderTypeExtraConfigSerializer
diff --git a/netbox_sys_plugin/navigation.py b/netbox_sys_plugin/navigation.py
index 1eea0d0..92c70c1 100644
--- a/netbox_sys_plugin/navigation.py
+++ b/netbox_sys_plugin/navigation.py
@@ -135,9 +135,14 @@ vmMachineItem = [
 operItem =[
     PluginMenuItem(
             link="plugins:netbox_sys_plugin:webhooksettings_list",
-            link_text="Webhook",
+            link_text="Webhook Settings",
             permissions=["netbox_sys_plugin.list_webhooksettings"],
         ),
+    PluginMenuItem(
+            link="plugins:netbox_sys_plugin:webhookpayloads_list",
+            link_text="Webhook Payload",
+            permissions=["netbox_sys_plugin.list_webhookpayloads"],
+        ),
 ]
 #Menu
 menu = PluginMenu(
diff --git a/netbox_sys_plugin/tables.py b/netbox_sys_plugin/tables.py
index 70ae2e5..5d21779 100644
--- a/netbox_sys_plugin/tables.py
+++ b/netbox_sys_plugin/tables.py
@@ -8,6 +8,7 @@ from .models import (
     VmAssignedVirtualMachineType,
     VirtualMachineType,
     WebhookSettings,
+    WebhookPayload,
     DomainNames,
     ProviderTypeExtraConfig,
     VmAssignedExtraConfig)
@@ -216,6 +217,32 @@ class WebhookSettingsTable(NetBoxTable):
 
         default_columns = ('id','payload_url','http_content_type')
 
+#WebhookPayload
+class WebhookPayloadTable(NetBoxTable):
+    """webhook Settings Table definition class"""
+
+    pk = columns.ToggleColumn()
+    id = tables.Column(
+        linkify=False,
+    )
+
+    payload_data = tables.Column(
+        linkify=False,
+        verbose_name="Payload Data",
+    )
+
+    class Meta(NetBoxTable.Meta):
+        """Meta class"""  
+        model = WebhookPayload
+        fields = (
+            "pk",
+            "id",
+            "payload_data",
+        )
+
+        default_columns = ('id','payload_data')
+
+
 #ExtraConfig
 class ProviderTypeExtraConfigTable(NetBoxTable):
     """Provider Type Extra Configs Table definition class"""
diff --git a/netbox_sys_plugin/tests/webhook_payload/__init__.py b/netbox_sys_plugin/tests/webhook_payload/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_sys_plugin/tests/webhook_payload/test_webhook_payload_api.py b/netbox_sys_plugin/tests/webhook_payload/test_webhook_payload_api.py
new file mode 100644
index 0000000..1907262
--- /dev/null
+++ b/netbox_sys_plugin/tests/webhook_payload/test_webhook_payload_api.py
@@ -0,0 +1,127 @@
+"""SYS Plugin Webhook payload API Test Case Class"""
+
+from users.models import ObjectPermission
+from django.contrib.contenttypes.models import ContentType
+from rest_framework import status
+from netbox_sys_plugin.models import WebhookPayload
+from ..base import BaseAPITestCase
+
+class WebhookPayloadApiTestCase(BaseAPITestCase):
+    """Test suite for WebhookPayload API"""
+    model = WebhookPayload
+    brief_fields = ["payload_data"]
+
+
+    @classmethod
+    def setUpTestData(cls):
+
+        """Set up test data for WebhookPayload API"""
+
+        # Data for valid creation
+        cls.valid_create_data = [
+            {
+                "payload_data":{'provider': {'id': 3, 'name': 'http://testtestesteste.com', 'provider_type': {'id': 2, 'name': 'AWS'}}},
+            }
+        ]
+
+        # Data for invalid creation
+        cls.invalid_create_data = [
+            {
+                "payload_data": None, # Missing
+
+            },
+        ]
+
+    def test_create_valid_webhook_payload(self):
+        """Test creating a valid webhook payload"""
+
+        obj_perm = ObjectPermission(
+            name="Create Webhook payload Permission",
+            actions=["add", "view"],
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ContentType.objects.get_for_model(WebhookPayload))
+
+        form_data = self.valid_create_data[0]
+        response = self.client.post(self._get_list_url(), form_data, format="json", **self.header)
+
+        self.assertHttpStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(response.data["payload_data"], form_data["payload_data"])
+
+    def test_create_invalid_webhook_payload(self):
+        """Test creating invalid webhook payload"""
+        obj_perm = ObjectPermission(
+            name="Invalid Webhook Payload Permission",
+            actions=["add", "view"],
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ContentType.objects.get_for_model(WebhookPayload))
+
+        form_data = self.invalid_create_data[0]
+        response = self.client.post(self._get_list_url(), form_data, format="json", **self.header)
+        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+        self.assertIn('This field may not be null.',str(response.data))
+
+    def test_get_webhook_payload_list(self):
+        """Test fetching webhook payload list"""
+
+        # Create Webhook payload
+        WebhookPayload.objects.create(
+            payload_data={'provider': {'id': 3, 'name': 'http://testtestesteste.com', 'provider_type': {'id': 2, 'name': 'AWS'}}},
+        )
+
+        obj_perm = ObjectPermission(
+            name="Create Webhook Payload Permission",
+            actions=["view"],
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ContentType.objects.get_for_model(WebhookPayload))
+
+        response = self.client.get(self._get_list_url(), **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertGreaterEqual(len(response.data), 2)
+
+    def test_get_single_webhook_payload(self):
+        """Test fetching a single webhook Payload"""
+
+        # Create Webhook payload
+        WebhookPayload.objects.create(
+            payload_data={'provider': {'id': 3, 'name': 'http://testtestesteste.com', 'provider_type': {'id': 2, 'name': 'AWS'}}},
+        )
+
+        obj_perm = ObjectPermission(
+            name="View Webhook payload Permission",
+            actions=["view"],
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ContentType.objects.get_for_model(WebhookPayload))
+
+        webhook_payload = WebhookPayload.objects.first()
+        response = self.client.get(self._get_detail_url(webhook_payload), **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response.data["payload_data"], webhook_payload.payload_data)
+
+    def test_delete_webhook_payload(self):
+        """Test deleting a webhook payload"""
+
+        # Create Webhook payload
+        WebhookPayload.objects.create(
+            payload_data={'provider': {'id': 3, 'name': 'http://testtestesteste.com', 'provider_type': {'id': 2, 'name': 'AWS'}}},
+        )
+
+        webhook_payload = WebhookPayload.objects.first()
+        obj_perm = ObjectPermission(
+            name="Delete Webhook payload Permission",
+            actions=["delete"],
+        )
+        obj_perm.save()
+        obj_perm.users.add(self.user)
+        obj_perm.object_types.add(ContentType.objects.get_for_model(WebhookPayload))
+
+        response = self.client.delete(self._get_detail_url(webhook_payload), **self.header)
+        self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
+        self.assertFalse(WebhookPayload.objects.filter(id=webhook_payload.id).exists())
diff --git a/netbox_sys_plugin/urls.py b/netbox_sys_plugin/urls.py
index fb24174..7c8ea9e 100644
--- a/netbox_sys_plugin/urls.py
+++ b/netbox_sys_plugin/urls.py
@@ -68,6 +68,15 @@ urlpatterns = (
         'model': models.WebhookSettings}),
     path('webhook-settings/<int:pk>/journal/', ObjectJournalView.as_view(), name='webhooksettings_journal', kwargs={
         'model': models.WebhookSettings}),
+    #WebhookPayload
+    path('webhook-payload/<int:pk>/', views.WebhookPayloadView.as_view(), name='webhookpayload'),
+    path('webhook-payload/', views.WebhookPayloadListView.as_view(), name='webhookpayloads_list'),
+    path('webhook-payload/<int:pk>/delete/', views.WebhookPayloadDeleteView.as_view(), name='webhookpayload_delete'),
+    path('webhook-payload/<int:pk>/edit/', views.WebhookPayloadEditView.as_view(), name='webhookpayload_edit'),
+    path('webhook-payload/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='webhookpayload_changelog', kwargs={
+        'model': models.WebhookPayload}),
+    path('webhook-payload/<int:pk>/journal/', ObjectJournalView.as_view(), name='webhookpayload_journal', kwargs={
+        'model': models.WebhookPayload}),
     #Extra Config
     path('extra-config/<int:pk>/', views.ProviderTypeExtraConfigView.as_view(), name='providertypeextraconfig'),
     path('extra-config/', views.ProviderTypeExtraConfigListView.as_view(), name='providertypeextraconfig_list'),
diff --git a/netbox_sys_plugin/views.py b/netbox_sys_plugin/views.py
index 977300c..bbd8d28 100644
--- a/netbox_sys_plugin/views.py
+++ b/netbox_sys_plugin/views.py
@@ -205,6 +205,31 @@ class WebhookSettingsDeleteView(generic.ObjectDeleteView):
     """Webhook Settings Delete delete view definition"""
     queryset = models.WebhookSettings.objects.all()
 
+#Webhook Payload
+class  WebhookPayloadView(generic.ObjectView):
+
+    """ Webhook Payload view definition"""
+
+    queryset = (
+        models.WebhookPayload.objects.all()
+    )
+
+class WebhookPayloadListView(generic.ObjectListView):
+    """Webhook Payload list view definition"""
+
+    queryset = models.WebhookPayload.objects.all()
+    table = tables.WebhookPayloadTable
+
+class WebhookPayloadDeleteView(generic.ObjectDeleteView):
+    """Webhook Payload Delete delete view definition"""
+    queryset = models.WebhookPayload.objects.all()
+
+class WebhookPayloadEditView(generic.ObjectEditView):
+    """
+    Defines the edit view for the Webhook Payload django model.
+    """
+    queryset = models.WebhookPayload.objects.all()
+
 #ExtraConfig
 class ProviderTypeExtraConfigView(generic.ObjectView):
     """ Virtual Machine Type view definition"""
-- 
GitLab