From 0de8b0785f324b9bc24e0ba688f4f810aaa9d120 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Mon, 23 Dec 2024 11:31:41 +0000
Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=93=9D=20Add=20extra=20configuration?=
 =?UTF-8?q?=20data=20model=20&=20general=20fixes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 docs/model/netbox_sys_provider_credentials.md          |  2 +-
 docs/model/netbox_sys_provider_type_extra_config.md    | 10 ++++++++++
 docs/model/netbox_sys_vm_assigned_extra_config.md      | 10 ++++++++++
 .../netbox_sys_vm_assigned_virtual_machine_type.md     |  4 ++--
 netbox_sys_plugin/tables.py                            |  6 +++---
 5 files changed, 26 insertions(+), 6 deletions(-)
 create mode 100644 docs/model/netbox_sys_provider_type_extra_config.md
 create mode 100644 docs/model/netbox_sys_vm_assigned_extra_config.md

diff --git a/docs/model/netbox_sys_provider_credentials.md b/docs/model/netbox_sys_provider_credentials.md
index 0876ed0..8c3b9ec 100644
--- a/docs/model/netbox_sys_provider_credentials.md
+++ b/docs/model/netbox_sys_provider_credentials.md
@@ -1,6 +1,6 @@
 ## netbox_sys_provider_credentials
 
-**Mapping Redirect definition class**
+**Provider Credentials definition class**
 
 | 	FIELD	                    | 	TYPE	                        | 	DESCRIPTION	                                                            |
 |-------------------------------|-----------------------------------|---------------------------------------------------------------------------|
diff --git a/docs/model/netbox_sys_provider_type_extra_config.md b/docs/model/netbox_sys_provider_type_extra_config.md
new file mode 100644
index 0000000..bcfdf9c
--- /dev/null
+++ b/docs/model/netbox_sys_provider_type_extra_config.md
@@ -0,0 +1,10 @@
+## netbox_sys_provider_type_extra_config
+
+**Provider Type Extra Configurations definition class**
+
+| 	FIELD	                    | 	TYPE	                        | 	DESCRIPTION	                                                            |
+|-------------------------------|-----------------------------------|---------------------------------------------------------------------------|
+| 	id	                        | 	Big (8 byte) integer	        | 	Unique ID	                                                            |
+| 	assigned_object_type_id     | 	Big (8 byte) integer	        | 	cluster type type ID	                                                |
+| 	assigned_object_id	        | 	Big (8 byte) integer	        | 	cluster type unique ID	                                                |
+| 	network_extra_config        | 	JSON                 	        | 	Provider type network extra configurations                              |
\ No newline at end of file
diff --git a/docs/model/netbox_sys_vm_assigned_extra_config.md b/docs/model/netbox_sys_vm_assigned_extra_config.md
new file mode 100644
index 0000000..a0d232a
--- /dev/null
+++ b/docs/model/netbox_sys_vm_assigned_extra_config.md
@@ -0,0 +1,10 @@
+## netbox_sys_vm_assigned_provider_type_extra_config
+
+**VM Assigned Provider Type Extra Configuration definition class**
+
+| 	FIELD	                             | 	TYPE	                        | 	DESCRIPTION	                                                            |
+|-------------------------------         |----------------------------------|---------------------------------------------------------------------------|
+| 	id	                                 | 	Big (8 byte) integer	        | 	Unique ID	                                                            |
+| 	assigned_object_type_id              | 	Big (8 byte) integer	        | 	Virtual Machine type type ID                                            |
+| 	assigned_object_id	                 | 	Big (8 byte) integer	        | 	Virtual Machine unique ID	                                            |
+| 	extra_config                         | 	JSON                 	        | 	Json with extra configuration with the structure from provider type     |
\ No newline at end of file
diff --git a/docs/model/netbox_sys_vm_assigned_virtual_machine_type.md b/docs/model/netbox_sys_vm_assigned_virtual_machine_type.md
index 791b6da..5bbd583 100644
--- a/docs/model/netbox_sys_vm_assigned_virtual_machine_type.md
+++ b/docs/model/netbox_sys_vm_assigned_virtual_machine_type.md
@@ -1,6 +1,6 @@
-## netbox_sys_virtual_machine_type
+## netbox_sys_vm_assigned_virtual_machine_type
 
-**Mapping Redirect definition class**
+**VM assigned Virtual Machine Type definition class**
 
 | 	FIELD	                             | 	TYPE	                        | 	DESCRIPTION	                                                            |
 |-------------------------------         |-----------------------------------|---------------------------------------------------------------------------|
diff --git a/netbox_sys_plugin/tables.py b/netbox_sys_plugin/tables.py
index cac807e..6c1ca84 100644
--- a/netbox_sys_plugin/tables.py
+++ b/netbox_sys_plugin/tables.py
@@ -2,7 +2,7 @@
 
 import django_tables2 as tables
 from netbox.tables import NetBoxTable, columns
-from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, WebhookSettings
+from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, WebhookSettings, DomainNames
 
 #VmMaintenance
 class VmMaintenanceTable(NetBoxTable):
@@ -148,7 +148,7 @@ class VmTypeTable(NetBoxTable):
 
 #DomainNames
 class DomainNamesTable(NetBoxTable):
-    """VM Maintenance Table definition class"""
+    """Domain Names Table definition class"""
 
     pk = columns.ToggleColumn()
     id = tables.Column(
@@ -168,7 +168,7 @@ class DomainNamesTable(NetBoxTable):
 
     class Meta(NetBoxTable.Meta):
         """Meta class"""  
-        model = VirtualMachineType
+        model = DomainNames
         fields = (
             "pk",
             "id",
-- 
GitLab


From b23c5d06648f09c705700c2dfb279ca5d6c52c7f Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Tue, 24 Dec 2024 11:45:06 +0000
Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=A8=20Add=20necessary=20objects=20to?=
 =?UTF-8?q?=20manage=20extra=20configs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../netbox_sys_provider_type_extra_config.md  |  2 +-
 docs/model/netbox_sys_virtual_machine_type.md |  2 +-
 netbox_sys_plugin/api/serializers.py          | 11 ++-
 netbox_sys_plugin/api/urls.py                 |  1 +
 netbox_sys_plugin/api/views.py                | 10 ++-
 netbox_sys_plugin/filtersets.py               | 28 ++++++-
 netbox_sys_plugin/forms/provider.py           | 82 ++++++++++++++++++-
 .../0010_providertypeextraconfig.py           | 37 +++++++++
 ...ypeextraconfig_extra_config_description.py | 18 ++++
 netbox_sys_plugin/models/provider.py          | 51 +++++++++++-
 netbox_sys_plugin/navigation.py               | 20 ++++-
 netbox_sys_plugin/tables.py                   | 32 +++++++-
 netbox_sys_plugin/template_content.py         |  3 +
 .../clustertype_providertypeextraconfig.html  | 39 +++++++++
 .../providertypeextraconfig.html              | 48 +++++++++++
 netbox_sys_plugin/urls.py                     |  9 +-
 netbox_sys_plugin/views.py                    | 29 ++++++-
 17 files changed, 404 insertions(+), 18 deletions(-)
 create mode 100644 netbox_sys_plugin/migrations/0010_providertypeextraconfig.py
 create mode 100644 netbox_sys_plugin/migrations/0011_providertypeextraconfig_extra_config_description.py
 create mode 100644 netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html
 create mode 100644 netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html

diff --git a/docs/model/netbox_sys_provider_type_extra_config.md b/docs/model/netbox_sys_provider_type_extra_config.md
index bcfdf9c..7b7d3ce 100644
--- a/docs/model/netbox_sys_provider_type_extra_config.md
+++ b/docs/model/netbox_sys_provider_type_extra_config.md
@@ -7,4 +7,4 @@
 | 	id	                        | 	Big (8 byte) integer	        | 	Unique ID	                                                            |
 | 	assigned_object_type_id     | 	Big (8 byte) integer	        | 	cluster type type ID	                                                |
 | 	assigned_object_id	        | 	Big (8 byte) integer	        | 	cluster type unique ID	                                                |
-| 	network_extra_config        | 	JSON                 	        | 	Provider type network extra configurations                              |
\ No newline at end of file
+| 	extra_config        | 	JSON                 	        | 	Provider type extra configurations                              |
\ No newline at end of file
diff --git a/docs/model/netbox_sys_virtual_machine_type.md b/docs/model/netbox_sys_virtual_machine_type.md
index 3fe0881..4881755 100644
--- a/docs/model/netbox_sys_virtual_machine_type.md
+++ b/docs/model/netbox_sys_virtual_machine_type.md
@@ -1,6 +1,6 @@
 ## netbox_sys_virtual_machine_type
 
-**Mapping Redirect definition class**
+**Virtual Machine Type definition class**
 
 | 	FIELD	                    | 	TYPE	                        | 	DESCRIPTION	                                                            |
 |-------------------------------|-----------------------------------|---------------------------------------------------------------------------|
diff --git a/netbox_sys_plugin/api/serializers.py b/netbox_sys_plugin/api/serializers.py
index a09b18a..2e0a433 100644
--- a/netbox_sys_plugin/api/serializers.py
+++ b/netbox_sys_plugin/api/serializers.py
@@ -8,7 +8,7 @@ from dcim.api.nested_serializers import (
 from ipam.api.nested_serializers import (
     NestedIPAddressSerializer, NestedServiceSerializer
 )
-from .. models import VirtualMachineMaintenance, ProviderCredentials,VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings
+from .. models import VirtualMachineMaintenance, ProviderCredentials,VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings,ProviderTypeExtraConfig
 from django.contrib.contenttypes.models import ContentType
 from .nested_serializers import * 
 from virtualization.choices import *
@@ -102,6 +102,15 @@ class WebhookSettingsSerializer(NetBoxModelSerializer):
         model = WebhookSettings
         fields = '__all__'
 
+class ProviderTypeExtraConfigSerializer(NetBoxModelSerializer):
+    id = serializers.IntegerField(read_only=True)
+    extra_config = serializers.JSONField()
+
+    class Meta:
+        model = ProviderTypeExtraConfig
+        fields = '__all__'
+
+
 
 class VirtualMachineSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
diff --git a/netbox_sys_plugin/api/urls.py b/netbox_sys_plugin/api/urls.py
index 3c43e5e..780e6ee 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'ProviderTypeExtraConfig', ProviderTypeExtraConfigViewSet)
 router.register(r'VirtualMachine', VirtualMachineViewSet)
 
 app_name = "netbox_sys_plugin"
diff --git a/netbox_sys_plugin/api/views.py b/netbox_sys_plugin/api/views.py
index ed9e993..f7b6604 100644
--- a/netbox_sys_plugin/api/views.py
+++ b/netbox_sys_plugin/api/views.py
@@ -4,8 +4,8 @@ from rest_framework.response import Response
 from django.db.models import Prefetch
 from netbox.api.viewsets import NetBoxModelViewSet
 from virtualization import filtersets
-from .. filtersets import VmTypeFilterSet
-from .. models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings
+from .. filtersets import VmTypeFilterSet, ProviderTypeExtraConfigFilterSet
+from .. models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings, ProviderTypeExtraConfig
 from . serializers import *
 
 class VirtualMachineMaintenanceViewSet(viewsets.ModelViewSet):
@@ -36,6 +36,12 @@ class VirtualMachineTypeViewSet(viewsets.ModelViewSet):
     filterset_class = VmTypeFilterSet
     http_method_names = ["get", "post", "patch", "delete", "options"]
 
+class ProviderTypeExtraConfigViewSet(viewsets.ModelViewSet):
+    queryset = ProviderTypeExtraConfig.objects.all()
+    serializer_class = ProviderTypeExtraConfigSerializer
+    filterset_class = ProviderTypeExtraConfigFilterSet
+    http_method_names = ["get", "post", "patch", "delete", "options"]
+
 class VirtualMachineViewSet(NetBoxModelViewSet):
     queryset = VirtualMachine.objects.prefetch_related(
         'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags', 'services'
diff --git a/netbox_sys_plugin/filtersets.py b/netbox_sys_plugin/filtersets.py
index 5b44013..d33d8af 100644
--- a/netbox_sys_plugin/filtersets.py
+++ b/netbox_sys_plugin/filtersets.py
@@ -4,7 +4,7 @@ import django_filters
 from netbox.filtersets import NetBoxModelFilterSet
 from virtualization.models import VirtualMachine, Cluster, ClusterType
 from django.db.models import Q
-from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType
+from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, ProviderTypeExtraConfig
 
 
 class VmMaintenanceFilterSet(NetBoxModelFilterSet):
@@ -100,4 +100,28 @@ class ProviderCredentialsFilterSet(NetBoxModelFilterSet):
         """override"""
         if not value.strip():
             return queryset
-        return queryset.filter(Q(provider_username__icontains=value)|Q(provider_password_vault_url__icontains=value))
\ No newline at end of file
+        return queryset.filter(Q(provider_username__icontains=value)|Q(provider_password_vault_url__icontains=value))
+    
+class ProviderTypeExtraConfigFilterSet(NetBoxModelFilterSet):
+    """Provider Type Extra Config filterset definition class"""
+
+    cluster_type_id = django_filters.ModelMultipleChoiceFilter(
+        field_name="ClusterType__id",
+        queryset=ClusterType.objects.all(),
+        to_field_name="id",
+        label="Cluster Type (id)",
+    )
+
+    class Meta:
+        """Meta class"""     
+        model = ProviderTypeExtraConfig
+        fields = ('extra_config_description','cluster_type_id')
+
+    # pylint: disable=W0613
+    def search(self, queryset, name, value):
+        """override"""
+        if not value.strip():
+            return queryset
+        return queryset.filter(Q(extra_config_description__icontains=value))
+
+
diff --git a/netbox_sys_plugin/forms/provider.py b/netbox_sys_plugin/forms/provider.py
index 1823cb1..fef9a91 100644
--- a/netbox_sys_plugin/forms/provider.py
+++ b/netbox_sys_plugin/forms/provider.py
@@ -1,6 +1,7 @@
 """Forms definitions"""
 
-from virtualization.models import Cluster
+import json
+from virtualization.models import Cluster,ClusterType
 from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import gettext_lazy as _
 from django.core.exceptions import ValidationError
@@ -9,8 +10,8 @@ from netbox.forms import (
     NetBoxModelForm,
     NetBoxModelFilterSetForm,
 )
-from utilities.forms.fields import DynamicModelChoiceField
-from ..models import ProviderCredentials
+from utilities.forms.fields import DynamicModelChoiceField, JSONField
+from ..models import ProviderCredentials, ProviderTypeExtraConfig
 
 class ProviderCredentialsForm(NetBoxModelForm):
     """
@@ -82,4 +83,77 @@ class ProviderCredentialsForm(NetBoxModelForm):
 class ProviderCredentialsFilterForm(NetBoxModelFilterSetForm):
     """Provider Credentials filter form definition class"""
 
-    model = ProviderCredentials
\ No newline at end of file
+    model = ProviderCredentials
+
+class ProviderTypeExtraConfigForm(NetBoxModelForm):
+    """
+    GUI form to add or edit a Provider Type Extra Configs.
+    """
+
+    cluster_type = DynamicModelChoiceField(
+        queryset=ClusterType.objects.all(), required=True, label="Provider Type"
+    )
+
+    extra_config = JSONField(
+        label=_('Extra Config'),
+        required=True
+    )
+
+
+    def __init__(self, *args, **kwargs):
+        # Initialize helper selectors
+        instance = kwargs.get("instance")
+        initial = kwargs.get("initial", {}).copy()
+        if instance:
+            if isinstance(instance.assigned_object, ClusterType):
+                initial["cluster_type"] = instance.assigned_object
+        kwargs["initial"] = initial
+        super().__init__(*args, **kwargs)
+
+        if initial and 'extra_config' in initial:
+            if type(initial['extra_config']) is str:
+                initial['extra_config'] = json.loads(initial['extra_config'])
+
+        if instance:
+            current_cluster_type_id = instance.assigned_object.id if instance.assigned_object else None
+        else:
+            current_cluster_type_id = None
+
+        assigned_cluster_types = ProviderTypeExtraConfig.objects.filter(
+            assigned_object_type=ContentType.objects.get_for_model(ClusterType)
+        ).exclude(assigned_object_id=current_cluster_type_id).values_list('assigned_object_id', flat=True)
+        self.fields['cluster_type'].ClusterType = ClusterType.objects.exclude(id__in=assigned_cluster_types)
+
+    class Meta:
+        """Meta class"""
+        model = ProviderTypeExtraConfig
+        fields = ('cluster_type', 'extra_config', 'extra_config_description','tags')
+
+
+    def clean(self):
+        """
+        Validates form inputs before submitting:
+        """
+        super().clean()
+        
+        cluster_type = self.cleaned_data.get("cluster_type")
+
+        #Check if cluster is assigned corretly
+        if (not cluster_type):
+            raise ValidationError(
+                {"__all__": "Can't assign more than one Configuration to the same Provider Type"},
+            )
+        
+        if self.errors.get(f"cluster_type"):
+            return
+    def save(self, *args, **kwargs):
+        # Set assigned object
+        self.instance.assigned_object = (
+            self.cleaned_data.get("cluster_type")
+        )
+        return super().save(*args, **kwargs)
+    
+class ProviderTypeExtraConfigFilterForm(NetBoxModelFilterSetForm):
+    """Provider Credentials filter form definition class"""
+
+    model = ProviderTypeExtraConfig
\ No newline at end of file
diff --git a/netbox_sys_plugin/migrations/0010_providertypeextraconfig.py b/netbox_sys_plugin/migrations/0010_providertypeextraconfig.py
new file mode 100644
index 0000000..c2b159c
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0010_providertypeextraconfig.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.2.16 on 2024-12-23 14:46
+
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+import utilities.json
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('extras', '0098_webhook_custom_field_data_webhook_tags'),
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('netbox_sys_plugin', '0009_alter_domainnames_assigned_object_type_and_more'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ProviderTypeExtraConfig',
+            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)),
+                ('extra_config', models.JSONField(blank=True, null=True)),
+                ('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
+                ('assigned_object_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'virtualization'), ('model', 'ClusterType'))), null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
+                ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+            ],
+            options={
+                'verbose_name': 'Provider Extra Configuration',
+                'verbose_name_plural': 'Provider Extra Configurations',
+                'ordering': ['assigned_object_id'],
+                'unique_together': {('assigned_object_id',)},
+            },
+        ),
+    ]
diff --git a/netbox_sys_plugin/migrations/0011_providertypeextraconfig_extra_config_description.py b/netbox_sys_plugin/migrations/0011_providertypeextraconfig_extra_config_description.py
new file mode 100644
index 0000000..c5819cb
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0011_providertypeextraconfig_extra_config_description.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.16 on 2024-12-23 15:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('netbox_sys_plugin', '0010_providertypeextraconfig'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='providertypeextraconfig',
+            name='extra_config_description',
+            field=models.CharField(default=None, max_length=100),
+        ),
+    ]
diff --git a/netbox_sys_plugin/models/provider.py b/netbox_sys_plugin/models/provider.py
index 61a33cb..fd8b289 100644
--- a/netbox_sys_plugin/models/provider.py
+++ b/netbox_sys_plugin/models/provider.py
@@ -11,10 +11,11 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.db.models import Q
-from virtualization.models import Cluster
+from virtualization.models import Cluster, ClusterType
 from netbox.models import NetBoxModel
 
 CLUSTER_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="Cluster"))
+CLUSTER_TYPE_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="ClusterType"))
 
 
 
@@ -52,7 +53,7 @@ class ProviderCredentials(NetBoxModel):
 
 
     def __str__(self):
-        return f"for Cluster ID {self.assigned_object_id}"
+        return f"for Provider ID {self.assigned_object_id}"
 
     def get_absolute_url(self):
         """override"""
@@ -65,3 +66,49 @@ GenericRelation(
     related_query_name="Cluster",
 ).contribute_to_class(Cluster, "ProviderCredentials")
 
+
+class ProviderTypeExtraConfig(NetBoxModel):
+    """Cluster Type Extra configurations definition class"""
+  
+    extra_config = models.JSONField(
+        blank=True,
+        null=True,
+    )
+    extra_config_description = models.CharField(
+        default=None, blank=False, null=False,
+        max_length=100)
+
+    assigned_object_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to=CLUSTER_TYPE_ASSIGNMENT_MODELS,
+        on_delete=models.PROTECT,
+        null=True,
+        blank=True,
+    )
+    assigned_object_id = models.PositiveBigIntegerField(null=True, blank=True)
+    assigned_object = GenericForeignKey(
+        ct_field="assigned_object_type",
+        fk_field="assigned_object_id",
+    )
+
+    class Meta:
+        """Meta class"""
+        unique_together = ["assigned_object_id"]
+        ordering = ["assigned_object_id"]
+        verbose_name = "Provider Extra Configuration"
+        verbose_name_plural = "Provider Extra Configurations"
+
+
+    def __str__(self):
+        return f"for Provider Type ID {self.assigned_object_id}"
+
+    def get_absolute_url(self):
+        """override"""
+        return reverse("plugins:netbox_sys_plugin:providertypeextraconfig", args=[self.pk])
+
+GenericRelation(
+    to=ProviderTypeExtraConfig,
+    content_type_field="assigned_object_type",
+    object_id_field="assigned_object_id",
+    related_query_name="ClusterType",
+).contribute_to_class(ClusterType, "ProviderTypeExtraConfig")
\ No newline at end of file
diff --git a/netbox_sys_plugin/navigation.py b/netbox_sys_plugin/navigation.py
index e92953a..73d299c 100644
--- a/netbox_sys_plugin/navigation.py
+++ b/netbox_sys_plugin/navigation.py
@@ -27,6 +27,16 @@ cluster_provider_credentials_buttons = [
     ),
 ]
 
+cluster_type_extra_config_buttons = [
+    PluginMenuButton(
+        link="plugins:netbox_sys_plugin:providertypeextraconfig_add",
+        title="Add",
+        icon_class="mdi mdi-plus-thick",
+        color=ButtonColorChoices.GREEN,
+        permissions=["netbox_sys_plugin.add_providertypeextraconfig"],
+    ),
+]
+
 vm_assigned_machine_type_buttons = [
     PluginMenuButton(
         link="plugins:netbox_sys_plugin:vmassignedvirtualmachinetype_add",
@@ -61,13 +71,19 @@ create_domainnames_buttons = [
 
 #Items
 
-clusterProviderCredentialsItem = [
+clusterClusterTypeItem = [
     PluginMenuItem(
         link="plugins:netbox_sys_plugin:providercredentials_list",
         link_text="Credentials",
         buttons=cluster_provider_credentials_buttons,
         permissions=["netbox_sys_plugin.list_providercredentials"],
     ),
+    PluginMenuItem(
+        link="plugins:netbox_sys_plugin:providertypeextraconfig_list",
+        link_text="Extra Config",
+        buttons=cluster_type_extra_config_buttons,
+        permissions=["netbox_sys_plugin.list_providertypeextraconfig"],
+    ),
 ]
 
 vmMachineItem = [
@@ -114,7 +130,7 @@ menu = PluginMenu(
     label="System Squad",
     groups=(
         
-        ("Provider", clusterProviderCredentialsItem),
+        ("Provider", clusterClusterTypeItem),
         ("Virtual Machine", (vmMachineItem)),
         ("Operation", (operItem)),
             ),
diff --git a/netbox_sys_plugin/tables.py b/netbox_sys_plugin/tables.py
index 6c1ca84..2554bcb 100644
--- a/netbox_sys_plugin/tables.py
+++ b/netbox_sys_plugin/tables.py
@@ -2,7 +2,7 @@
 
 import django_tables2 as tables
 from netbox.tables import NetBoxTable, columns
-from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, WebhookSettings, DomainNames
+from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, WebhookSettings, DomainNames, ProviderTypeExtraConfig
 
 #VmMaintenance
 class VmMaintenanceTable(NetBoxTable):
@@ -208,3 +208,33 @@ class WebhookSettingsTable(NetBoxTable):
 
         default_columns = ('id','payload_url','http_content_type')
 
+#ExtraConfig
+class ProviderTypeExtraConfigTable(NetBoxTable):
+    """Extra Configs Table definition class"""
+
+    pk = columns.ToggleColumn()
+    id = tables.Column(
+        linkify=True,
+    )
+    
+    assigned_object = tables.Column(
+        linkify=True,
+        orderable=False,
+        verbose_name="Provider Type",
+    )
+
+    extra_config = tables.Column(
+        linkify=True,
+        verbose_name="Extra Config",
+    )
+
+    class Meta(NetBoxTable.Meta):
+        """Meta class"""  
+        model = ProviderTypeExtraConfig
+        fields = (
+            "pk",
+            "id",
+            "extra_config",
+        )
+
+        default_columns = ('id','assigned_object','extra_config')
diff --git a/netbox_sys_plugin/template_content.py b/netbox_sys_plugin/template_content.py
index 99b8625..91588d3 100644
--- a/netbox_sys_plugin/template_content.py
+++ b/netbox_sys_plugin/template_content.py
@@ -31,6 +31,9 @@ class VmTypeTable(PluginTemplateExtension):
 
     def right_page(self):
         return self.render('netbox_sys_plugin/clustertype_virtualmachinetype.html', extra_context={'vmtypesinfo': models.VirtualMachineType.objects.filter(ClusterType=self.context['object'])})
+    
+    def left_page(self):
+        return self.render('netbox_sys_plugin/clustertype_providertypeextraconfig.html', extra_context={'extraconfiginfo': models.ProviderTypeExtraConfig.objects.filter(ClusterType=self.context['object'])})
 
 
 class ProviderCredentialsTable(PluginTemplateExtension):
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html
new file mode 100644
index 0000000..0bc024e
--- /dev/null
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html
@@ -0,0 +1,39 @@
+{% block content %}
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">Virtual Machine Types</h5>
+        <div class="card-body">
+          <table class="table table-responsive">
+              <th scope="row">Extra Config</th>
+              <th scope="row">Description</th>
+              {% for extraconfig in extraconfiginfo %}
+        <tr>
+            <td>
+              {% if extraconfig.extra_config %}
+                <a href="{{ extraconfig.get_absolute_url }}">{{ extraconfig.extra_config }}</a>
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+            <td>
+              {% if extraconfig.extra_config_description %}
+                {{ extraconfig.extra_config_description }}
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+        </tr>
+        {% empty %}
+        <tr>
+            <td colspan="3">No Virtual Machine Types found.</td>
+        </tr>
+        {% endfor %}
+            </tr>
+          </table>
+        </div>
+      </div>
+      {% include 'inc/panels/custom_fields.html' %}
+    </div>
+  </div>
+{% endblock content %}
\ No newline at end of file
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
new file mode 100644
index 0000000..74ec628
--- /dev/null
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
@@ -0,0 +1,48 @@
+{% extends 'generic/object.html' %}
+
+{% block title%}
+Extra Config for {{ object.assigned_object }} 
+{% endblock title%}
+
+{% block content %}
+<div class="row mb-3">
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">Extra Configuration</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Extra Config</th>
+            <td>{% include 'extras/inc/configcontext_data.html' with data=object.extra_config format=json %}</td>
+          </tr>
+          <tr>
+            <th scope="row">Description</th>
+            <td>{{ object.extra_config_desc }}</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+    {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/tags.html' %}
+  </div>
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">Provider Type</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">{{ object.assigned_object_type|cut:"virtualization | " }}</th>
+            <td>
+              {% if object.assigned_object %}
+                <a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a>
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock content %}
diff --git a/netbox_sys_plugin/urls.py b/netbox_sys_plugin/urls.py
index 2d281af..711323d 100644
--- a/netbox_sys_plugin/urls.py
+++ b/netbox_sys_plugin/urls.py
@@ -56,5 +56,12 @@ urlpatterns = (
     path('webhook-settings/<int:pk>/delete/', views.WebhookSettingsDeleteView.as_view(), name='webhooksettings_delete'),
     path('webhook-settings/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='webhooksettings_changelog', kwargs={'model': models.WebhookSettings}),
     path('webhook-settings/<int:pk>/journal/', ObjectJournalView.as_view(), name='webhooksettings_journal', kwargs={'model': models.WebhookSettings}),
-
+    #Extra Config
+    path('extra-config/<int:pk>/', views.ProviderTypeExtraConfigView.as_view(), name='providertypeextraconfig'),
+    path('extra-config/', views.ProviderTypeExtraConfigListView.as_view(), name='providertypeextraconfig_list'),
+    path('extra-config/add/', views.ProviderTypeExtraConfigEditView.as_view(), name='providertypeextraconfig_add'),
+    path('extra-config/<int:pk>/edit/', views.ProviderTypeExtraConfigEditView.as_view(), name='providertypeextraconfig_edit'),
+    path('extra-config/<int:pk>/delete/', views.ProviderTypeExtraConfigDeleteView.as_view(), name='providertypeextraconfig_delete'),
+    path('extra-config/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providertypeextraconfig_changelog', kwargs={'model': models.ProviderTypeExtraConfig}),
+    path('extra-config/<int:pk>/journal/', ObjectJournalView.as_view(), name='providertypeextraconfig_journal', kwargs={'model': models.ProviderTypeExtraConfig}),
 )
diff --git a/netbox_sys_plugin/views.py b/netbox_sys_plugin/views.py
index 59ddfd7..759992f 100644
--- a/netbox_sys_plugin/views.py
+++ b/netbox_sys_plugin/views.py
@@ -197,4 +197,31 @@ class WebhookSettingsEditView(generic.ObjectEditView):
 
 class WebhookSettingsDeleteView(generic.ObjectDeleteView):
         
-    queryset = models.WebhookSettings.objects.all()
\ No newline at end of file
+    queryset = models.WebhookSettings.objects.all()
+
+#ExtraConfig
+class ProviderTypeExtraConfigView(generic.ObjectView):
+    """ Virtual Machine Type view definition"""
+
+    queryset = (
+        models.ProviderTypeExtraConfig.objects.all()
+    )
+
+class ProviderTypeExtraConfigListView(generic.ObjectListView):
+    """Cluster Type Virtual Machine Type list view definition"""
+
+    queryset = models.ProviderTypeExtraConfig.objects.all()
+    table = tables.ProviderTypeExtraConfigTable
+    filterset = filtersets.ProviderCredentialsFilterSet
+    filterset_form = forms.ProviderTypeExtraConfigFilterForm
+
+class ProviderTypeExtraConfigEditView(generic.ObjectEditView):
+    """
+    Defines the edit view for the Extra Configs django model.
+    """
+    queryset = models.ProviderTypeExtraConfig.objects.all()
+    form = forms.ProviderTypeExtraConfigForm
+
+class ProviderTypeExtraConfigDeleteView(generic.ObjectDeleteView):
+        
+    queryset = models.ProviderTypeExtraConfig.objects.all()
\ No newline at end of file
-- 
GitLab


From 1d94d0f521e286e71a816450282cca8ce49cbe8a Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Tue, 24 Dec 2024 16:02:19 +0000
Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=90=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/forms/createvm.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/netbox_sys_plugin/forms/createvm.py b/netbox_sys_plugin/forms/createvm.py
index 829acbc..8b78975 100644
--- a/netbox_sys_plugin/forms/createvm.py
+++ b/netbox_sys_plugin/forms/createvm.py
@@ -94,7 +94,7 @@ class IPAddressForm(NetBoxModelForm):
         super().__init__(*args, **kwargs)
         self.fields.pop('tags',None)
 
-class DomainNamesForm(NetBoxModelForm):
+class DomainNameForm(NetBoxModelForm):
     """Form for Domain Names."""
 
     domain_names = JSONField(
@@ -176,7 +176,7 @@ ClusterListFormSet = forms.modelformset_factory(
 )
 
 DomainNamesFormSet = forms.modelformset_factory(
-    DomainNames, form=DomainNamesForm, formset=DomainNamesForm, extra=1, can_delete=False
+    DomainNames, form=DomainNameForm, formset=DomainNameForm, extra=1, can_delete=False
 )
 
 VmAssignedVirtualMachineTypeFormSet = forms.modelformset_factory(
-- 
GitLab


From 3f3078ea84a6c7cdfb9a68f09c08db1a5c54e217 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Tue, 24 Dec 2024 16:23:36 +0000
Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=A6=BA=20Add=20duplicated=20key=20val?=
 =?UTF-8?q?idation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/forms/provider.py | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/netbox_sys_plugin/forms/provider.py b/netbox_sys_plugin/forms/provider.py
index fef9a91..97b91ae 100644
--- a/netbox_sys_plugin/forms/provider.py
+++ b/netbox_sys_plugin/forms/provider.py
@@ -19,7 +19,7 @@ class ProviderCredentialsForm(NetBoxModelForm):
     """
 
     cluster = DynamicModelChoiceField(
-        queryset=Cluster.objects.all(), required=True, label="Cluster"
+        queryset=Cluster.objects.all(), required=True, label="Provider"
     )
     provider_username = forms.CharField(
         max_length=50, min_length=1, required=True, label="Username"
@@ -67,7 +67,7 @@ class ProviderCredentialsForm(NetBoxModelForm):
         #Check if cluster is assigned corretly
         if (not cluster):
             raise ValidationError(
-                {"__all__": "Can't assign more than one Credential to the same Cluster"},
+                {"__all__": "Can't assign more than one Credential to the same Provider"},
             )
         
         if self.errors.get(f"provider_username"):
@@ -91,7 +91,9 @@ class ProviderTypeExtraConfigForm(NetBoxModelForm):
     """
 
     cluster_type = DynamicModelChoiceField(
-        queryset=ClusterType.objects.all(), required=True, label="Provider Type"
+        queryset=ClusterType.objects.all(),
+        required=True,
+        label="Provider Type"
     )
 
     extra_config = JSONField(
@@ -110,10 +112,6 @@ class ProviderTypeExtraConfigForm(NetBoxModelForm):
         kwargs["initial"] = initial
         super().__init__(*args, **kwargs)
 
-        if initial and 'extra_config' in initial:
-            if type(initial['extra_config']) is str:
-                initial['extra_config'] = json.loads(initial['extra_config'])
-
         if instance:
             current_cluster_type_id = instance.assigned_object.id if instance.assigned_object else None
         else:
@@ -135,17 +133,18 @@ class ProviderTypeExtraConfigForm(NetBoxModelForm):
         Validates form inputs before submitting:
         """
         super().clean()
-        
+
         cluster_type = self.cleaned_data.get("cluster_type")
+        cluster_type_ct = ContentType.objects.get_for_model(ClusterType)
 
-        #Check if cluster is assigned corretly
-        if (not cluster_type):
+        if ProviderTypeExtraConfig.objects.filter(
+            assigned_object_type=cluster_type_ct,
+            assigned_object_id=cluster_type.id
+        ).exclude(pk=self.instance.pk).exists():
             raise ValidationError(
-                {"__all__": "Can't assign more than one Configuration to the same Provider Type"},
+                {"__all__":"A Provider Type can only have one extra configuration"}
             )
         
-        if self.errors.get(f"cluster_type"):
-            return
     def save(self, *args, **kwargs):
         # Set assigned object
         self.instance.assigned_object = (
-- 
GitLab


From 6702b05b1bf6653cf802d964afdfd033e3db8f33 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Fri, 27 Dec 2024 10:56:14 +0000
Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=92=84=20Add=20pretty=20Json=20to=20U?=
 =?UTF-8?q?I?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../templates/netbox_sys_plugin/domainnames.html       |  4 ++--
 .../netbox_sys_plugin/providertypeextraconfig.html     |  3 ++-
 netbox_sys_plugin/templatetags/extras.py               | 10 ++++++++++
 3 files changed, 14 insertions(+), 3 deletions(-)
 create mode 100644 netbox_sys_plugin/templatetags/extras.py

diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/domainnames.html b/netbox_sys_plugin/templates/netbox_sys_plugin/domainnames.html
index a203221..f52b792 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/domainnames.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/domainnames.html
@@ -14,8 +14,8 @@
         <table class="table table-hover attr-table">
           <tr>
             <th scope="row">Domain Names</th>
-            <td>{% include 'extras/inc/configcontext_data.html' with data=object.domain_names format=json %}</td>
-            
+            {% load extras %}
+            <td><pre class="block">{{ object.domain_names | pretty_json }}</pre></td>
           </tr>
         </table>
       </div>
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
index 74ec628..ff047e2 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
@@ -13,7 +13,8 @@ Extra Config for {{ object.assigned_object }}
         <table class="table table-hover attr-table">
           <tr>
             <th scope="row">Extra Config</th>
-            <td>{% include 'extras/inc/configcontext_data.html' with data=object.extra_config format=json %}</td>
+            {% load extras %}
+            <td><pre class="block">{{ object.extra_config | pretty_json }}</pre></td>
           </tr>
           <tr>
             <th scope="row">Description</th>
diff --git a/netbox_sys_plugin/templatetags/extras.py b/netbox_sys_plugin/templatetags/extras.py
new file mode 100644
index 0000000..c40a78e
--- /dev/null
+++ b/netbox_sys_plugin/templatetags/extras.py
@@ -0,0 +1,10 @@
+import json
+
+from django import template
+
+register = template.Library()
+
+
+@register.filter
+def pretty_json(value):
+    return json.dumps(value, indent=4)
\ No newline at end of file
-- 
GitLab


From 5ad511ab5a9ad70538ddd3117ae86b14487e7d97 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Fri, 27 Dec 2024 11:08:01 +0000
Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=90=9B=20Add=20missing=20value=20to?=
 =?UTF-8?q?=20template?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../templates/netbox_sys_plugin/providertypeextraconfig.html    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
index ff047e2..05e7ebc 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
@@ -18,7 +18,7 @@ Extra Config for {{ object.assigned_object }}
           </tr>
           <tr>
             <th scope="row">Description</th>
-            <td>{{ object.extra_config_desc }}</td>
+            <td>{{ object.extra_config_description }}</td>
           </tr>
         </table>
       </div>
-- 
GitLab


From 7885006d6a80765f591167a4f878a06ec79f7bc6 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Fri, 27 Dec 2024 17:11:18 +0000
Subject: [PATCH 7/9] =?UTF-8?q?=E2=9C=A8=20Add=20assignment=20management?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/api/serializers.py          |  13 +-
 netbox_sys_plugin/api/urls.py                 |   2 +
 netbox_sys_plugin/api/views.py                |  13 +-
 netbox_sys_plugin/filtersets.py               |  11 +-
 netbox_sys_plugin/forms/machine.py            | 113 +++++++++---------
 netbox_sys_plugin/forms/provider.py           |  74 +++++++++++-
 .../migrations/0012_vmassignedextraconfig.py  |  38 ++++++
 ...aconfig_extra_config_structure_and_more.py |  23 ++++
 netbox_sys_plugin/models/machine.py           | 106 ++++++++--------
 netbox_sys_plugin/models/provider.py          |  68 +++++++++--
 netbox_sys_plugin/navigation.py               |  25 ++--
 netbox_sys_plugin/tables.py                   |  50 +++++++-
 .../providertypeextraconfig.html              |   6 +-
 .../vmassignedextraconfig.html                |  55 +++++++++
 netbox_sys_plugin/urls.py                     |   8 ++
 netbox_sys_plugin/views.py                    |  32 ++++-
 16 files changed, 493 insertions(+), 144 deletions(-)
 create mode 100644 netbox_sys_plugin/migrations/0012_vmassignedextraconfig.py
 create mode 100644 netbox_sys_plugin/migrations/0013_rename_extra_config_providertypeextraconfig_extra_config_structure_and_more.py
 create mode 100644 netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html

diff --git a/netbox_sys_plugin/api/serializers.py b/netbox_sys_plugin/api/serializers.py
index 2e0a433..4ca980e 100644
--- a/netbox_sys_plugin/api/serializers.py
+++ b/netbox_sys_plugin/api/serializers.py
@@ -8,7 +8,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
+from .. models import VirtualMachineMaintenance, ProviderCredentials,VmAssignedVirtualMachineType, VirtualMachineType,DomainNames, WebhookSettings,ProviderTypeExtraConfig,VmAssignedExtraConfig
 from django.contrib.contenttypes.models import ContentType
 from .nested_serializers import * 
 from virtualization.choices import *
@@ -104,13 +104,22 @@ class WebhookSettingsSerializer(NetBoxModelSerializer):
 
 class ProviderTypeExtraConfigSerializer(NetBoxModelSerializer):
     id = serializers.IntegerField(read_only=True)
-    extra_config = serializers.JSONField()
+    extra_config_structure = serializers.JSONField()
 
     class Meta:
         model = ProviderTypeExtraConfig
         fields = '__all__'
 
+class VmAssignedExtraConfigSerializer(NetBoxModelSerializer):
+    id = serializers.IntegerField(read_only=True)
+    virtual_machine = VirtualMachineSerializer(source='assigned_object', read_only=True)
+    assigned_object_id = serializers.IntegerField(write_only=True)
+    assigned_object_type = serializers.CharField(write_only=True)
+    display = serializers.CharField(source="__str__")
 
+    class Meta:
+        model = VmAssignedExtraConfig
+        fields = '__all__'
 
 class VirtualMachineSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
diff --git a/netbox_sys_plugin/api/urls.py b/netbox_sys_plugin/api/urls.py
index 780e6ee..cd2d3e1 100644
--- a/netbox_sys_plugin/api/urls.py
+++ b/netbox_sys_plugin/api/urls.py
@@ -10,6 +10,8 @@ router.register(r'DomainNames', DomainNamesViewSet)
 router.register(r'WebhookSettings', WebhookSettingsViewSet)
 router.register(r'ProviderTypeExtraConfig', ProviderTypeExtraConfigViewSet)
 router.register(r'VirtualMachine', VirtualMachineViewSet)
+router.register(r'ExtraConfig', ProviderTypeExtraConfigViewSet)
+router.register(r'AssignedExtraConfig', VmAssignedExtraConfigViewSet)
 
 app_name = "netbox_sys_plugin"
 
diff --git a/netbox_sys_plugin/api/views.py b/netbox_sys_plugin/api/views.py
index f7b6604..b3a1030 100644
--- a/netbox_sys_plugin/api/views.py
+++ b/netbox_sys_plugin/api/views.py
@@ -53,4 +53,15 @@ class VirtualMachineViewSet(NetBoxModelViewSet):
 class WebhookSettingsViewSet(viewsets.ModelViewSet):
     queryset = WebhookSettings.objects.all()
     serializer_class = WebhookSettingsSerializer
-    http_method_names = ["get", "post", "patch", "delete", "options"]
\ No newline at end of file
+    http_method_names = ["get", "post", "patch", "delete", "options"]
+
+class ProviderTypeExtraConfigViewSet(viewsets.ModelViewSet):
+    queryset = ProviderTypeExtraConfig.objects.all()
+    serializer_class = ProviderTypeExtraConfigSerializer
+    http_method_names = ["get", "post", "patch", "delete", "options"]
+
+class VmAssignedExtraConfigViewSet(viewsets.ModelViewSet):
+    queryset = VmAssignedExtraConfig.objects.all()
+    serializer_class = VmAssignedExtraConfigSerializer
+    http_method_names = ["get", "post", "patch", "delete", "options"]
+
diff --git a/netbox_sys_plugin/filtersets.py b/netbox_sys_plugin/filtersets.py
index d33d8af..45dcbae 100644
--- a/netbox_sys_plugin/filtersets.py
+++ b/netbox_sys_plugin/filtersets.py
@@ -4,7 +4,7 @@ import django_filters
 from netbox.filtersets import NetBoxModelFilterSet
 from virtualization.models import VirtualMachine, Cluster, ClusterType
 from django.db.models import Q
-from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, ProviderTypeExtraConfig
+from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, ProviderTypeExtraConfig, VmAssignedExtraConfig
 
 
 class VmMaintenanceFilterSet(NetBoxModelFilterSet):
@@ -124,4 +124,13 @@ class ProviderTypeExtraConfigFilterSet(NetBoxModelFilterSet):
             return queryset
         return queryset.filter(Q(extra_config_description__icontains=value))
 
+class VmAssignedExtraConfigFilterSet(NetBoxModelFilterSet):
+    """Vm Assigned Extra Config filterset definition class"""
+
+
+    class Meta:
+        """Meta class"""     
+        model = VmAssignedExtraConfig
+        fields = ('assigned_object_type','provider_type_extra_config_assignment_desc', 'provider_type_extra_config')
+
 
diff --git a/netbox_sys_plugin/forms/machine.py b/netbox_sys_plugin/forms/machine.py
index a217ed7..d7d860c 100644
--- a/netbox_sys_plugin/forms/machine.py
+++ b/netbox_sys_plugin/forms/machine.py
@@ -11,7 +11,7 @@ from netbox.forms import (
     NetBoxModelFilterSetForm,
 )
 from utilities.forms.fields import DynamicModelChoiceField, JSONField
-from ..models import VmAssignedVirtualMachineType, VirtualMachineMaintenance, VirtualMachineType, DomainNames
+from ..models import VmAssignedVirtualMachineType, VirtualMachineMaintenance, DomainNames, VmAssignedExtraConfig
 
 class VmAssignedVmTypeForm(NetBoxModelForm):
     """
@@ -82,80 +82,88 @@ class VmAssignedVmTypeFilterForm(NetBoxModelFilterSetForm):
 
     model = VmAssignedVirtualMachineType
 
-class VmTypeForm(NetBoxModelForm):
+class VmMaitenanceForm(NetBoxModelForm):
     """
-    GUI form to add or edit a Virtual Machine Type.
+    GUI form to add or edit a VM Maitenance.
     """
-
-    cluster_type = DynamicModelChoiceField(
-        queryset=ClusterType.objects.all(), required=True, label="Cluster Type"
-    )
-    virtual_machine_type_name = forms.CharField(
-        max_length=200, min_length=1, required=True, label="Name"
+    virtual_machine = DynamicModelChoiceField(
+        queryset=VirtualMachine.objects.none(), required=True, label="Virtual Machine"
     )
-    virtual_machine_type_desc = forms.CharField(
-        max_length=50, min_length=1, required=False, label="Description"
+    maintenance_window = forms.CharField(
+        max_length=7, min_length=1, required=True, label="Maintenance Window",
+        help_text=mark_safe(
+            "<b>*Note:</b> Day of the week and a time of the day, 0 = Sunday , 1 = monday. Format 0-00:00",
+        ),
     )
 
-
     def __init__(self, *args, **kwargs):
         # Initialize helper selectors
         instance = kwargs.get("instance")
         initial = kwargs.get("initial", {}).copy()
         if instance:
-            if isinstance(instance.assigned_object, ClusterType):
-                initial["cluster_type"] = instance.assigned_object
+            if isinstance(instance.assigned_object, VirtualMachine):
+                initial["virtual_machine"] = instance.assigned_object
         kwargs["initial"] = initial
         super().__init__(*args, **kwargs)
 
+        if instance:
+            current_vm_id = instance.assigned_object.id if instance.assigned_object else None
+        else:
+            current_vm_id = None
+
+        assigned_vms = VirtualMachineMaintenance.objects.filter(
+            assigned_object_type=ContentType.objects.get_for_model(VirtualMachine)
+        ).exclude(assigned_object_id=current_vm_id).values_list('assigned_object_id', flat=True)
+        self.fields['virtual_machine'].queryset = VirtualMachine.objects.exclude(id__in=assigned_vms)
 
     class Meta:
         """Meta class"""
-        model = VirtualMachineType
-        fields = ('cluster_type','virtual_machine_type_name', 'virtual_machine_type_desc','tags')
+        model = VirtualMachineMaintenance
+        fields = ( 'virtual_machine','maintenance_window','tags')
+
 
     def clean(self):
         """
         Validates form inputs before submitting:
         """
         super().clean()
+        
+        
+        vm = self.cleaned_data.get("virtual_machine")
 
-        virtual_machine_type_name = self.cleaned_data.get("virtual_machine_type_name")
-        cluster_type = self.cleaned_data.get("cluster_type")
-        cluster_type_ct = ContentType.objects.get_for_model(ClusterType)
-
-        if VirtualMachineType.objects.filter(
-            virtual_machine_type_name =virtual_machine_type_name,
-            assigned_object_type=cluster_type_ct,
-            assigned_object_id=cluster_type.id
-        ).exclude(pk=self.instance.pk).exists():
+        #Check if Virtual Machine is assigned corretly
+        if (not vm):
             raise ValidationError(
-                {"__all__":"A Type with this name and cluster type already exists"}
+                {"__all__": "Can't assign more than one Maintenance Window to the same Virtual Machine"},
             )
+        
+        if self.errors.get("virtual_machine"):
+            return
+
 
 
     def save(self, *args, **kwargs):
         # Set assigned object
-        self.instance.assigned_object = self.cleaned_data.get("cluster_type")
+        self.instance.assigned_object = (
+            self.cleaned_data.get("virtual_machine") 
+        )
         return super().save(*args, **kwargs)
 
-class VmTypeFilterForm(NetBoxModelFilterSetForm):
-    """Virtual Machine Type filter form definition class"""
+class VmMaintenanceFilterForm(NetBoxModelFilterSetForm):
+    """MacAddress filter form definition class"""
 
-    model = VirtualMachineType
+    model = VirtualMachineMaintenance
 
-class VmMaitenanceForm(NetBoxModelForm):
+class DomainNamesForm(NetBoxModelForm):
     """
     GUI form to add or edit a VM Maitenance.
     """
     virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.none(), required=True, label="Virtual Machine"
     )
-    maintenance_window = forms.CharField(
-        max_length=7, min_length=1, required=True, label="Maintenance Window",
-        help_text=mark_safe(
-            "<b>*Note:</b> Day of the week and a time of the day, 0 = Sunday , 1 = monday. Format 0-00:00",
-        ),
+    domain_names = JSONField(
+        label=_('Domain Names'),
+        required=False
     )
 
     def __init__(self, *args, **kwargs):
@@ -173,15 +181,15 @@ class VmMaitenanceForm(NetBoxModelForm):
         else:
             current_vm_id = None
 
-        assigned_vms = VirtualMachineMaintenance.objects.filter(
+        assigned_vms = DomainNames.objects.filter(
             assigned_object_type=ContentType.objects.get_for_model(VirtualMachine)
         ).exclude(assigned_object_id=current_vm_id).values_list('assigned_object_id', flat=True)
         self.fields['virtual_machine'].queryset = VirtualMachine.objects.exclude(id__in=assigned_vms)
 
     class Meta:
         """Meta class"""
-        model = VirtualMachineMaintenance
-        fields = ( 'virtual_machine','maintenance_window','tags')
+        model = DomainNames
+        fields = ( 'virtual_machine','domain_names','tags')
 
 
     def clean(self):
@@ -211,22 +219,18 @@ class VmMaitenanceForm(NetBoxModelForm):
         )
         return super().save(*args, **kwargs)
 
-class VmMaintenanceFilterForm(NetBoxModelFilterSetForm):
+class DomainNamesFilterForm(NetBoxModelFilterSetForm):
     """MacAddress filter form definition class"""
 
     model = VirtualMachineMaintenance
 
-class DomainNamesForm(NetBoxModelForm):
+class VmAssignedExtraConfigForm(NetBoxModelForm):
     """
-    GUI form to add or edit a VM Maitenance.
+    GUI form to add or edit a Virtual Machine Type.
     """
     virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.none(), required=True, label="Virtual Machine"
     )
-    domain_names = JSONField(
-        label=_('Domain Names'),
-        required=False
-    )
 
     def __init__(self, *args, **kwargs):
         # Initialize helper selectors
@@ -243,16 +247,15 @@ class DomainNamesForm(NetBoxModelForm):
         else:
             current_vm_id = None
 
-        assigned_vms = DomainNames.objects.filter(
+        assigned_vms = VmAssignedExtraConfig.objects.filter(
             assigned_object_type=ContentType.objects.get_for_model(VirtualMachine)
         ).exclude(assigned_object_id=current_vm_id).values_list('assigned_object_id', flat=True)
         self.fields['virtual_machine'].queryset = VirtualMachine.objects.exclude(id__in=assigned_vms)
 
     class Meta:
         """Meta class"""
-        model = DomainNames
-        fields = ( 'virtual_machine','domain_names','tags')
-
+        model = VmAssignedExtraConfig
+        fields = ('virtual_machine','provider_type_extra_config', 'provider_type_extra_config_assignment_desc','tags')
 
     def clean(self):
         """
@@ -266,14 +269,12 @@ class DomainNamesForm(NetBoxModelForm):
         #Check if Virtual Machine is assigned corretly
         if (not vm):
             raise ValidationError(
-                {"__all__": "Can't assign more than one Maintenance Window to the same Virtual Machine"},
+                {"__all__": "Can't assign more than one Extra Config to the same Virtual Machine"},
             )
         
         if self.errors.get("virtual_machine"):
             return
 
-
-
     def save(self, *args, **kwargs):
         # Set assigned object
         self.instance.assigned_object = (
@@ -281,7 +282,7 @@ class DomainNamesForm(NetBoxModelForm):
         )
         return super().save(*args, **kwargs)
 
-class DomainNamesFilterForm(NetBoxModelFilterSetForm):
-    """MacAddress filter form definition class"""
+class VmAssignedExtraConfigFilterForm(NetBoxModelFilterSetForm):
+    """Virtual Machine Type filter form definition class"""
 
-    model = VirtualMachineMaintenance
\ No newline at end of file
+    model = VmAssignedExtraConfig
\ No newline at end of file
diff --git a/netbox_sys_plugin/forms/provider.py b/netbox_sys_plugin/forms/provider.py
index 97b91ae..e966127 100644
--- a/netbox_sys_plugin/forms/provider.py
+++ b/netbox_sys_plugin/forms/provider.py
@@ -11,7 +11,7 @@ from netbox.forms import (
     NetBoxModelFilterSetForm,
 )
 from utilities.forms.fields import DynamicModelChoiceField, JSONField
-from ..models import ProviderCredentials, ProviderTypeExtraConfig
+from ..models import ProviderCredentials, ProviderTypeExtraConfig, VirtualMachineType
 
 class ProviderCredentialsForm(NetBoxModelForm):
     """
@@ -85,6 +85,68 @@ class ProviderCredentialsFilterForm(NetBoxModelFilterSetForm):
 
     model = ProviderCredentials
 
+class VmTypeForm(NetBoxModelForm):
+    """
+    GUI form to add or edit a Virtual Machine Type.
+    """
+
+    cluster_type = DynamicModelChoiceField(
+        queryset=ClusterType.objects.all(), required=True, label="Cluster Type"
+    )
+    virtual_machine_type_name = forms.CharField(
+        max_length=200, min_length=1, required=True, label="Name"
+    )
+    virtual_machine_type_desc = forms.CharField(
+        max_length=50, min_length=1, required=False, label="Description"
+    )
+
+
+    def __init__(self, *args, **kwargs):
+        # Initialize helper selectors
+        instance = kwargs.get("instance")
+        initial = kwargs.get("initial", {}).copy()
+        if instance:
+            if isinstance(instance.assigned_object, ClusterType):
+                initial["cluster_type"] = instance.assigned_object
+        kwargs["initial"] = initial
+        super().__init__(*args, **kwargs)
+
+
+    class Meta:
+        """Meta class"""
+        model = VirtualMachineType
+        fields = ('cluster_type','virtual_machine_type_name', 'virtual_machine_type_desc','tags')
+
+    def clean(self):
+        """
+        Validates form inputs before submitting:
+        """
+        super().clean()
+
+        virtual_machine_type_name = self.cleaned_data.get("virtual_machine_type_name")
+        cluster_type = self.cleaned_data.get("cluster_type")
+        cluster_type_ct = ContentType.objects.get_for_model(ClusterType)
+
+        if VirtualMachineType.objects.filter(
+            virtual_machine_type_name =virtual_machine_type_name,
+            assigned_object_type=cluster_type_ct,
+            assigned_object_id=cluster_type.id
+        ).exclude(pk=self.instance.pk).exists():
+            raise ValidationError(
+                {"__all__":"A Type with this name and cluster type already exists"}
+            )
+
+
+    def save(self, *args, **kwargs):
+        # Set assigned object
+        self.instance.assigned_object = self.cleaned_data.get("cluster_type")
+        return super().save(*args, **kwargs)
+
+class VmTypeFilterForm(NetBoxModelFilterSetForm):
+    """Virtual Machine Type filter form definition class"""
+
+    model = VirtualMachineType
+
 class ProviderTypeExtraConfigForm(NetBoxModelForm):
     """
     GUI form to add or edit a Provider Type Extra Configs.
@@ -96,12 +158,18 @@ class ProviderTypeExtraConfigForm(NetBoxModelForm):
         label="Provider Type"
     )
 
-    extra_config = JSONField(
+    extra_config_name = forms.CharField(
+        label=_('Extra Config Name'),
+        required=True
+    )
+
+    extra_config_structure = JSONField(
         label=_('Extra Config'),
         required=True
     )
 
 
+
     def __init__(self, *args, **kwargs):
         # Initialize helper selectors
         instance = kwargs.get("instance")
@@ -125,7 +193,7 @@ class ProviderTypeExtraConfigForm(NetBoxModelForm):
     class Meta:
         """Meta class"""
         model = ProviderTypeExtraConfig
-        fields = ('cluster_type', 'extra_config', 'extra_config_description','tags')
+        fields = ('cluster_type', 'extra_config_name','extra_config_structure', 'extra_config_description','tags')
 
 
     def clean(self):
diff --git a/netbox_sys_plugin/migrations/0012_vmassignedextraconfig.py b/netbox_sys_plugin/migrations/0012_vmassignedextraconfig.py
new file mode 100644
index 0000000..8bb9b79
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0012_vmassignedextraconfig.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.2.16 on 2024-12-27 12:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+import utilities.json
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('extras', '0098_webhook_custom_field_data_webhook_tags'),
+        ('netbox_sys_plugin', '0011_providertypeextraconfig_extra_config_description'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='VmAssignedExtraConfig',
+            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)),
+                ('provider_type_extra_config_assignment_desc', models.CharField(blank=True, default=None, max_length=100, null=True)),
+                ('assigned_object_id', models.PositiveBigIntegerField(blank=True, null=True)),
+                ('assigned_object_type', models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'virtualization'), ('model', 'virtualmachine'))), null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype')),
+                ('provider_type_extra_config', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assigned_vm_type', to='netbox_sys_plugin.providertypeextraconfig')),
+                ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+            ],
+            options={
+                'verbose_name': 'Assigned Extra Config',
+                'verbose_name_plural': 'Assigned Extra Configs',
+                'ordering': ['assigned_object_id'],
+                'unique_together': {('assigned_object_id',)},
+            },
+        ),
+    ]
diff --git a/netbox_sys_plugin/migrations/0013_rename_extra_config_providertypeextraconfig_extra_config_structure_and_more.py b/netbox_sys_plugin/migrations/0013_rename_extra_config_providertypeextraconfig_extra_config_structure_and_more.py
new file mode 100644
index 0000000..4de7a83
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0013_rename_extra_config_providertypeextraconfig_extra_config_structure_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.16 on 2024-12-27 16:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('netbox_sys_plugin', '0012_vmassignedextraconfig'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='providertypeextraconfig',
+            old_name='extra_config',
+            new_name='extra_config_structure',
+        ),
+        migrations.AddField(
+            model_name='providertypeextraconfig',
+            name='extra_config_name',
+            field=models.CharField(blank=True, default=None, max_length=50, null=True),
+        ),
+    ]
diff --git a/netbox_sys_plugin/models/machine.py b/netbox_sys_plugin/models/machine.py
index 8ccb800..42222a0 100644
--- a/netbox_sys_plugin/models/machine.py
+++ b/netbox_sys_plugin/models/machine.py
@@ -4,9 +4,6 @@ from django.core.validators import (
     RegexValidator,
 )
 from django.urls import reverse
-from django.core.validators import (
-    URLValidator,    
-)  # pylint: disable=import-error
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
@@ -14,55 +11,6 @@ from virtualization.models import VirtualMachine, ClusterType
 from netbox.models import NetBoxModel
 
 VM_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="virtualmachine"))
-CLUSTERTYPE_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="ClusterType"))
-
-#VM Type
-class VirtualMachineType(NetBoxModel):
-    """Cluster ProviderInfo definition class"""
-
-    virtual_machine_type_name = models.CharField(
-        default=None, blank=False, null=False,
-        max_length=50,
-        )
-    
-    virtual_machine_type_desc = models.CharField(
-        default=None, blank=False, null=False,
-        max_length=100)
-
-    assigned_object_type = models.ForeignKey(
-        to=ContentType,
-        limit_choices_to=CLUSTERTYPE_ASSIGNMENT_MODELS,
-        on_delete=models.PROTECT,
-        null=True,
-        blank=True,
-    )
-    assigned_object_id = models.PositiveBigIntegerField(null=True, blank=True)
-    assigned_object = GenericForeignKey(
-        ct_field="assigned_object_type",
-        fk_field="assigned_object_id",
-    )
-
-    class Meta:
-        """Meta class"""
-        unique_together = ["assigned_object_id","virtual_machine_type_name"]
-        ordering = ["assigned_object_id"]
-        verbose_name = "Virtual Machine Type"
-        verbose_name_plural = "Virtual Machine Types"
-
-
-    def __str__(self):
-        return self.virtual_machine_type_name
-
-    def get_absolute_url(self):
-        """override"""
-        return reverse("plugins:netbox_sys_plugin:virtualmachinetype", args=[self.pk])
-
-GenericRelation(
-    to=VirtualMachineType,
-    content_type_field="assigned_object_type",
-    object_id_field="assigned_object_id",
-    related_query_name="ClusterType",
-).contribute_to_class(ClusterType, "VirtualMachineType")
 
 #VM Assigned Type
 class VmAssignedVirtualMachineType(NetBoxModel):
@@ -208,4 +156,56 @@ GenericRelation(
     content_type_field="assigned_object_type",
     object_id_field="assigned_object_id",
     related_query_name="VirtualMachine",
-).contribute_to_class(VirtualMachine, "DomainNames")
\ No newline at end of file
+).contribute_to_class(VirtualMachine, "DomainNames")
+
+#VM Assigned Extra Config
+class VmAssignedExtraConfig(NetBoxModel):
+    """Virtual Machine Type Info definition class"""
+  
+    provider_type_extra_config = models.ForeignKey(
+        to="ProviderTypeExtraConfig",
+        on_delete=models.PROTECT,
+        null=False,
+        blank=False,
+        related_name="assigned_vm_type",
+        name='provider_type_extra_config'
+    )
+    
+    provider_type_extra_config_assignment_desc = models.CharField(
+        default=None, blank=True, null=True,
+        max_length=100)
+
+    assigned_object_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to=VM_ASSIGNMENT_MODELS,
+        on_delete=models.PROTECT,
+        null=True,
+        blank=True,
+    )
+    assigned_object_id = models.PositiveBigIntegerField(null=True, blank=True)
+    assigned_object = GenericForeignKey(
+        ct_field="assigned_object_type",
+        fk_field="assigned_object_id",
+    )
+
+    class Meta:
+        """Meta class"""
+        unique_together = ["assigned_object_id"]
+        ordering = ["assigned_object_id"]
+        verbose_name = "Assigned Extra Config"
+        verbose_name_plural = "Assigned Extra Configs"
+
+
+    def __str__(self):
+        return f"Virtual machine ID {self.assigned_object_id}"
+
+    def get_absolute_url(self):
+        """override"""
+        return reverse("plugins:netbox_sys_plugin:vmassignedextraconfig", args=[self.pk])
+
+GenericRelation(
+    to=VmAssignedExtraConfig,
+    content_type_field="assigned_object_type",
+    object_id_field="assigned_object_id",
+    related_query_name="VirtualMachine",
+).contribute_to_class(VirtualMachine, "VmAssignedExtraConfig")
diff --git a/netbox_sys_plugin/models/provider.py b/netbox_sys_plugin/models/provider.py
index fd8b289..f116282 100644
--- a/netbox_sys_plugin/models/provider.py
+++ b/netbox_sys_plugin/models/provider.py
@@ -1,24 +1,16 @@
 """Models definitions"""
 
-from django.core.validators import (
-    RegexValidator,
-)
 from django.urls import reverse
-from django.core.validators import (
-    URLValidator,    
-)  # pylint: disable=import-error
 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
-from django.db.models import Q
 from virtualization.models import Cluster, ClusterType
 from netbox.models import NetBoxModel
 
 CLUSTER_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="Cluster"))
 CLUSTER_TYPE_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="ClusterType"))
 
-
-
+#Provider Credentials
 class ProviderCredentials(NetBoxModel):
     """Cluster ProviderInfo definition class"""
   
@@ -66,11 +58,63 @@ GenericRelation(
     related_query_name="Cluster",
 ).contribute_to_class(Cluster, "ProviderCredentials")
 
+#Virtual Machine Type
+class VirtualMachineType(NetBoxModel):
+    """Cluster ProviderInfo definition class"""
+
+    virtual_machine_type_name = models.CharField(
+        default=None, blank=False, null=False,
+        max_length=50,
+        )
+    
+    virtual_machine_type_desc = models.CharField(
+        default=None, blank=False, null=False,
+        max_length=100)
 
+    assigned_object_type = models.ForeignKey(
+        to=ContentType,
+        limit_choices_to=CLUSTER_TYPE_ASSIGNMENT_MODELS,
+        on_delete=models.PROTECT,
+        null=True,
+        blank=True,
+    )
+    assigned_object_id = models.PositiveBigIntegerField(null=True, blank=True)
+    assigned_object = GenericForeignKey(
+        ct_field="assigned_object_type",
+        fk_field="assigned_object_id",
+    )
+
+    class Meta:
+        """Meta class"""
+        unique_together = ["assigned_object_id","virtual_machine_type_name"]
+        ordering = ["assigned_object_id"]
+        verbose_name = "Virtual Machine Type"
+        verbose_name_plural = "Virtual Machine Types"
+
+
+    def __str__(self):
+        return self.virtual_machine_type_name
+
+    def get_absolute_url(self):
+        """override"""
+        return reverse("plugins:netbox_sys_plugin:virtualmachinetype", args=[self.pk])
+
+GenericRelation(
+    to=VirtualMachineType,
+    content_type_field="assigned_object_type",
+    object_id_field="assigned_object_id",
+    related_query_name="ClusterType",
+).contribute_to_class(ClusterType, "VirtualMachineType")
+
+#Provider Extra Config
 class ProviderTypeExtraConfig(NetBoxModel):
     """Cluster Type Extra configurations definition class"""
   
-    extra_config = models.JSONField(
+    extra_config_name = models.CharField(
+        default=None, blank=True, null=True,
+        max_length=50)
+
+    extra_config_structure = models.JSONField(
         blank=True,
         null=True,
     )
@@ -100,7 +144,7 @@ class ProviderTypeExtraConfig(NetBoxModel):
 
 
     def __str__(self):
-        return f"for Provider Type ID {self.assigned_object_id}"
+        return f"{ self.extra_config_name }"
 
     def get_absolute_url(self):
         """override"""
@@ -111,4 +155,4 @@ GenericRelation(
     content_type_field="assigned_object_type",
     object_id_field="assigned_object_id",
     related_query_name="ClusterType",
-).contribute_to_class(ClusterType, "ProviderTypeExtraConfig")
\ No newline at end of file
+).contribute_to_class(ClusterType, "ProviderTypeExtraConfig")
diff --git a/netbox_sys_plugin/navigation.py b/netbox_sys_plugin/navigation.py
index 73d299c..0b651bd 100644
--- a/netbox_sys_plugin/navigation.py
+++ b/netbox_sys_plugin/navigation.py
@@ -71,13 +71,21 @@ create_domainnames_buttons = [
 
 #Items
 
-clusterClusterTypeItem = [
+clusterItem = [
     PluginMenuItem(
         link="plugins:netbox_sys_plugin:providercredentials_list",
         link_text="Credentials",
         buttons=cluster_provider_credentials_buttons,
         permissions=["netbox_sys_plugin.list_providercredentials"],
     ),
+]
+ClusterTypeItem = [
+    PluginMenuItem(
+        link="plugins:netbox_sys_plugin:virtualmachinetype_list",
+        link_text="Virtual Machine Type",
+        buttons=vm_machine_type_buttons,
+        permissions=["netbox_sys_plugin.list_vmmachinetype"],
+    ),
     PluginMenuItem(
         link="plugins:netbox_sys_plugin:providertypeextraconfig_list",
         link_text="Extra Config",
@@ -87,18 +95,18 @@ clusterClusterTypeItem = [
 ]
 
 vmMachineItem = [
-    PluginMenuItem(
-        link="plugins:netbox_sys_plugin:virtualmachinetype_list",
-        link_text="Type",
-        buttons=vm_machine_type_buttons,
-        permissions=["netbox_sys_plugin.list_vmmachinetype"],
-    ),
     PluginMenuItem(
         link="plugins:netbox_sys_plugin:vmassignedvirtualmachinetype_list",
         link_text="Type Assignment",
         buttons=vm_assigned_machine_type_buttons,
         permissions=["netbox_sys_plugin.list_vmassignedmachinetype"],
     ),
+    PluginMenuItem(
+        link="plugins:netbox_sys_plugin:vmassignedextraconfig_list",
+        link_text="Extra Config Assignment",
+        buttons=vm_assigned_machine_type_buttons,
+        permissions=["netbox_sys_plugin.list_vmassignedextraconfig"],
+    ),
     PluginMenuItem(
         link="plugins:netbox_sys_plugin:virtualmachinemaintenance_list",
         link_text="Maintenance",
@@ -130,7 +138,8 @@ menu = PluginMenu(
     label="System Squad",
     groups=(
         
-        ("Provider", clusterClusterTypeItem),
+        ("Provider", clusterItem),
+        ("Provider Type", ClusterTypeItem),
         ("Virtual Machine", (vmMachineItem)),
         ("Operation", (operItem)),
             ),
diff --git a/netbox_sys_plugin/tables.py b/netbox_sys_plugin/tables.py
index 2554bcb..b19abd0 100644
--- a/netbox_sys_plugin/tables.py
+++ b/netbox_sys_plugin/tables.py
@@ -2,7 +2,7 @@
 
 import django_tables2 as tables
 from netbox.tables import NetBoxTable, columns
-from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, WebhookSettings, DomainNames, ProviderTypeExtraConfig
+from .models import VirtualMachineMaintenance, ProviderCredentials, VmAssignedVirtualMachineType, VirtualMachineType, WebhookSettings, DomainNames, ProviderTypeExtraConfig, VmAssignedExtraConfig
 
 #VmMaintenance
 class VmMaintenanceTable(NetBoxTable):
@@ -222,10 +222,14 @@ class ProviderTypeExtraConfigTable(NetBoxTable):
         orderable=False,
         verbose_name="Provider Type",
     )
+    extra_config_name = tables.Column(
+        linkify=True,
+        verbose_name="Extra Config Name",
+    )
 
-    extra_config = tables.Column(
+    extra_config_structure = tables.Column(
         linkify=True,
-        verbose_name="Extra Config",
+        verbose_name="Extra Config Structure",
     )
 
     class Meta(NetBoxTable.Meta):
@@ -234,7 +238,43 @@ class ProviderTypeExtraConfigTable(NetBoxTable):
         fields = (
             "pk",
             "id",
-            "extra_config",
+            "extra_config_name",
+            "extra_config_structure",
+        )
+
+        default_columns = ('id','assigned_object','extra_config_name','extra_config_structure')
+
+#VmAssignedType
+class VmAssignedExtraConfig(NetBoxTable):
+    """VM Maintenance Table definition class"""
+
+    pk = columns.ToggleColumn()
+    id = tables.Column(
+        linkify=True,
+    )
+    
+    assigned_object = tables.Column(
+        linkify=True,
+        orderable=False,
+        verbose_name="Virtual Machine",
+    )
+    provider_type_extra_config = tables.Column(
+        linkify=True,
+        verbose_name="Extra Configuration",
+    )
+    provider_type_extra_config_assignment_desc = tables.Column(
+        linkify=False,
+        verbose_name="Description",
+    )
+
+    class Meta(NetBoxTable.Meta):
+        """Meta class"""  
+        model = VmAssignedExtraConfig
+        fields = (
+            "pk",
+            "id",
+            "provider_type_extra_config",
+            "provider_type_extra_config_assignment_desc", 
         )
 
-        default_columns = ('id','assigned_object','extra_config')
+        default_columns = ('id','assigned_object','provider_type_extra_config','provider_type_extra_config_assignment_desc')
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
index 05e7ebc..b92dc21 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
@@ -11,10 +11,14 @@ Extra Config for {{ object.assigned_object }}
       <h5 class="card-header">Extra Configuration</h5>
       <div class="card-body">
         <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Extra Config Name</th>
+            <td>{{ object.extra_config_name }}</td>
+          </tr>
           <tr>
             <th scope="row">Extra Config</th>
             {% load extras %}
-            <td><pre class="block">{{ object.extra_config | pretty_json }}</pre></td>
+            <td><pre class="block">{{ object.extra_config_structure | pretty_json }}</pre></td>
           </tr>
           <tr>
             <th scope="row">Description</th>
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html
new file mode 100644
index 0000000..8747cf0
--- /dev/null
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html
@@ -0,0 +1,55 @@
+{% extends 'generic/object.html' %}
+
+{% block title%}
+Extra Config for {{ object.assigned_object }} 
+{% endblock title%}
+
+{% block content %}
+<div class="row mb-3">
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">Virtual Machine Extra Config Assignement </h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Name</th>
+            <td>
+              {% if object.provider_type_extra_config %}
+                <a href="{{ object.provider_type_extra_config.get_absolute_url }}">{{ object.provider_type_extra_config }}</a>
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+          </tr>
+
+          <tr>
+            <th scope="row">Description</th>
+            <td>{{ object.provider_type_extra_config_assignment_desc }}</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+    {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/tags.html' %}
+  </div>
+  <div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">Virtual Machine</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">{{ object.assigned_object_type|cut:"virtualization | " }}</th>
+            <td>
+              {% if object.assigned_object %}
+                <a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a>
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock content %}
diff --git a/netbox_sys_plugin/urls.py b/netbox_sys_plugin/urls.py
index 711323d..fc8bbab 100644
--- a/netbox_sys_plugin/urls.py
+++ b/netbox_sys_plugin/urls.py
@@ -64,4 +64,12 @@ urlpatterns = (
     path('extra-config/<int:pk>/delete/', views.ProviderTypeExtraConfigDeleteView.as_view(), name='providertypeextraconfig_delete'),
     path('extra-config/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providertypeextraconfig_changelog', kwargs={'model': models.ProviderTypeExtraConfig}),
     path('extra-config/<int:pk>/journal/', ObjectJournalView.as_view(), name='providertypeextraconfig_journal', kwargs={'model': models.ProviderTypeExtraConfig}),
+    #virtualMachineAssignedExtraConfig
+    path('vm-assigned-extra-config/<int:pk>/', views.VmAssignedExtraConfigView.as_view(), name='vmassignedextraconfig'),
+    path('vm-assigned-extra-config/', views.VmAssignedExtraConfigListView.as_view(), name='vmassignedextraconfig_list'),
+    path('vm-assigned-extra-config/add/', views.VmAssignedExtraConfigEditView.as_view(), name='vmassignedextraconfig_add'),
+    path('vm-assigned-extra-config/<int:pk>/edit/', views.VmAssignedExtraConfigEditView.as_view(), name='vmassignedextraconfig_edit'),
+    path('vm-assigned-extra-config/<int:pk>/delete/', views.VmAssignedExtraConfigDeleteView.as_view(), name='vmassignedextraconfig_delete'),
+    path('vm-assigned-extra-config/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vmassignedextraconfig_changelog', kwargs={'model': models.VmAssignedExtraConfig}),
+    path('vm-assigned-extra-config/<int:pk>/journal/', ObjectJournalView.as_view(), name='vmassignedextraconfig_journal', kwargs={'model': models.VmAssignedExtraConfig}),
 )
diff --git a/netbox_sys_plugin/views.py b/netbox_sys_plugin/views.py
index 759992f..0e2d120 100644
--- a/netbox_sys_plugin/views.py
+++ b/netbox_sys_plugin/views.py
@@ -65,7 +65,7 @@ class ProviderCredentialsEditView(generic.ObjectEditView):
     form = forms.ProviderCredentialsForm
 
 #VmAssignedType
-class  VmAssignedVmTypeView(generic.ObjectView):
+class VmAssignedVmTypeView(generic.ObjectView):
     """ Virtual Machine Type view definition"""
 
     queryset = (
@@ -224,4 +224,32 @@ class ProviderTypeExtraConfigEditView(generic.ObjectEditView):
 
 class ProviderTypeExtraConfigDeleteView(generic.ObjectDeleteView):
         
-    queryset = models.ProviderTypeExtraConfig.objects.all()
\ No newline at end of file
+    queryset = models.ProviderTypeExtraConfig.objects.all()
+
+#VmAssignedExtraConfig
+class VmAssignedExtraConfigView(generic.ObjectView):
+    """ Virtual Machine Assigned Extra Config view definition"""
+
+    queryset = (
+        models.VmAssignedExtraConfig.objects.all()
+    )
+
+class VmAssignedExtraConfigListView(generic.ObjectListView):
+    """Virtual Machine Assigned Extra Config list view definition"""
+
+    queryset = models.VmAssignedExtraConfig.objects.all()
+    table = tables.VmAssignedExtraConfig
+    filterset = filtersets.VmAssignedExtraConfigFilterSet
+    filterset_form = forms.VmAssignedExtraConfigFilterForm
+
+class VmAssignedExtraConfigDeleteView(generic.ObjectDeleteView):
+    """Virtual Machine Assigned Extra Config delete view definition"""
+
+    queryset = models.VmAssignedExtraConfig.objects.all()
+
+class VmAssignedExtraConfigEditView(generic.ObjectEditView):
+    """
+    Defines the edit view for the Virtual Machine Assigned Extra Config django model.
+    """
+    queryset = models.VmAssignedExtraConfig.objects.all()
+    form = forms.VmAssignedExtraConfigForm
-- 
GitLab


From 70b30f1b8e1ce00005a2fa9bd3bbef496ebc53e3 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Tue, 31 Dec 2024 11:06:47 +0000
Subject: [PATCH 8/9] =?UTF-8?q?=E2=9C=A8=20Complete=20extra=20config=20man?=
 =?UTF-8?q?agement=20objects?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/forms/machine.py            | 13 +++++-
 ...assignedextraconfig_extra_config_values.py | 18 ++++++++
 ...dextraconfig_provider_type_extra_config.py | 19 +++++++++
 netbox_sys_plugin/models/machine.py           |  7 +++-
 netbox_sys_plugin/navigation.py               | 11 ++++-
 netbox_sys_plugin/tables.py                   |  8 ++--
 netbox_sys_plugin/template_content.py         |  3 ++
 .../providertypeextraconfig.html              |  4 +-
 .../vm_vmassignedextraconfig.html             | 41 +++++++++++++++++++
 9 files changed, 114 insertions(+), 10 deletions(-)
 create mode 100644 netbox_sys_plugin/migrations/0014_vmassignedextraconfig_extra_config_values.py
 create mode 100644 netbox_sys_plugin/migrations/0015_alter_vmassignedextraconfig_provider_type_extra_config.py
 create mode 100644 netbox_sys_plugin/templates/netbox_sys_plugin/vm_vmassignedextraconfig.html

diff --git a/netbox_sys_plugin/forms/machine.py b/netbox_sys_plugin/forms/machine.py
index d7d860c..4d8c21b 100644
--- a/netbox_sys_plugin/forms/machine.py
+++ b/netbox_sys_plugin/forms/machine.py
@@ -11,7 +11,7 @@ from netbox.forms import (
     NetBoxModelFilterSetForm,
 )
 from utilities.forms.fields import DynamicModelChoiceField, JSONField
-from ..models import VmAssignedVirtualMachineType, VirtualMachineMaintenance, DomainNames, VmAssignedExtraConfig
+from ..models import VmAssignedVirtualMachineType, VirtualMachineMaintenance, DomainNames, VmAssignedExtraConfig, ProviderTypeExtraConfig
 
 class VmAssignedVmTypeForm(NetBoxModelForm):
     """
@@ -232,6 +232,15 @@ class VmAssignedExtraConfigForm(NetBoxModelForm):
         queryset=VirtualMachine.objects.none(), required=True, label="Virtual Machine"
     )
 
+    provider_type_extra_config = DynamicModelChoiceField(
+        queryset=ProviderTypeExtraConfig.objects.all(),
+        required=True,
+        label="Provider Type Extra Config",
+        query_params={
+            'extra_config_structure__isnull': False  # Example: Filter configs with non-null structures
+        }
+    )
+
     def __init__(self, *args, **kwargs):
         # Initialize helper selectors
         instance = kwargs.get("instance")
@@ -255,7 +264,7 @@ class VmAssignedExtraConfigForm(NetBoxModelForm):
     class Meta:
         """Meta class"""
         model = VmAssignedExtraConfig
-        fields = ('virtual_machine','provider_type_extra_config', 'provider_type_extra_config_assignment_desc','tags')
+        fields = ('virtual_machine','provider_type_extra_config','extra_config_values','provider_type_extra_config_assignment_desc','tags')
 
     def clean(self):
         """
diff --git a/netbox_sys_plugin/migrations/0014_vmassignedextraconfig_extra_config_values.py b/netbox_sys_plugin/migrations/0014_vmassignedextraconfig_extra_config_values.py
new file mode 100644
index 0000000..d55ed6a
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0014_vmassignedextraconfig_extra_config_values.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.16 on 2024-12-30 10:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('netbox_sys_plugin', '0013_rename_extra_config_providertypeextraconfig_extra_config_structure_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='vmassignedextraconfig',
+            name='extra_config_values',
+            field=models.JSONField(null=True),
+        ),
+    ]
diff --git a/netbox_sys_plugin/migrations/0015_alter_vmassignedextraconfig_provider_type_extra_config.py b/netbox_sys_plugin/migrations/0015_alter_vmassignedextraconfig_provider_type_extra_config.py
new file mode 100644
index 0000000..81d305c
--- /dev/null
+++ b/netbox_sys_plugin/migrations/0015_alter_vmassignedextraconfig_provider_type_extra_config.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.16 on 2024-12-30 12:16
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('netbox_sys_plugin', '0014_vmassignedextraconfig_extra_config_values'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='vmassignedextraconfig',
+            name='provider_type_extra_config',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='provider_type_extra_config', to='netbox_sys_plugin.providertypeextraconfig'),
+        ),
+    ]
diff --git a/netbox_sys_plugin/models/machine.py b/netbox_sys_plugin/models/machine.py
index 42222a0..805fbab 100644
--- a/netbox_sys_plugin/models/machine.py
+++ b/netbox_sys_plugin/models/machine.py
@@ -167,9 +167,14 @@ class VmAssignedExtraConfig(NetBoxModel):
         on_delete=models.PROTECT,
         null=False,
         blank=False,
-        related_name="assigned_vm_type",
+        related_name="provider_type_extra_config",
         name='provider_type_extra_config'
     )
+
+    extra_config_values = models.JSONField(
+        blank=False,
+        null=True,
+    )
     
     provider_type_extra_config_assignment_desc = models.CharField(
         default=None, blank=True, null=True,
diff --git a/netbox_sys_plugin/navigation.py b/netbox_sys_plugin/navigation.py
index 0b651bd..97b3b44 100644
--- a/netbox_sys_plugin/navigation.py
+++ b/netbox_sys_plugin/navigation.py
@@ -46,6 +46,15 @@ vm_assigned_machine_type_buttons = [
         permissions=["netbox_sys_plugin.add_vmassignedmachinetype"],
     ),
 ]
+vm_assigned_extra_config_buttons = [
+    PluginMenuButton(
+        link="plugins:netbox_sys_plugin:vmassignedextraconfig_add",
+        title="Add",
+        icon_class="mdi mdi-plus-thick",
+        color=ButtonColorChoices.GREEN,
+        permissions=["netbox_sys_plugin.add_vmassignedextraconfig"],
+    ),
+]
 
 vm_machine_type_buttons = [
     PluginMenuButton(
@@ -104,7 +113,7 @@ vmMachineItem = [
     PluginMenuItem(
         link="plugins:netbox_sys_plugin:vmassignedextraconfig_list",
         link_text="Extra Config Assignment",
-        buttons=vm_assigned_machine_type_buttons,
+        buttons=vm_assigned_extra_config_buttons,
         permissions=["netbox_sys_plugin.list_vmassignedextraconfig"],
     ),
     PluginMenuItem(
diff --git a/netbox_sys_plugin/tables.py b/netbox_sys_plugin/tables.py
index b19abd0..02dcb4c 100644
--- a/netbox_sys_plugin/tables.py
+++ b/netbox_sys_plugin/tables.py
@@ -254,13 +254,13 @@ class VmAssignedExtraConfig(NetBoxTable):
     )
     
     assigned_object = tables.Column(
-        linkify=True,
-        orderable=False,
+        linkify=False,
+        orderable=True,
         verbose_name="Virtual Machine",
     )
     provider_type_extra_config = tables.Column(
-        linkify=True,
-        verbose_name="Extra Configuration",
+        linkify=False,
+        verbose_name="Extra Configuration Structure",
     )
     provider_type_extra_config_assignment_desc = tables.Column(
         linkify=False,
diff --git a/netbox_sys_plugin/template_content.py b/netbox_sys_plugin/template_content.py
index 91588d3..43bc604 100644
--- a/netbox_sys_plugin/template_content.py
+++ b/netbox_sys_plugin/template_content.py
@@ -22,6 +22,9 @@ class VmTableDomainNames(PluginTemplateExtension):
     
     def left_page(self):
         return self.render('netbox_sys_plugin/vm_domainnames.html', extra_context={'domainnamesinfo': models.DomainNames.objects.filter(VirtualMachine=self.context['object'])})
+    
+    def full_width_page(self):
+        return self.render('netbox_sys_plugin/vm_vmassignedextraconfig.html', extra_context={'vmassignextraconfiginfo': models.VmAssignedExtraConfig.objects.filter(VirtualMachine=self.context['object'])})
 
 
 class VmTypeTable(PluginTemplateExtension):
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
index b92dc21..6b57ccd 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/providertypeextraconfig.html
@@ -1,7 +1,7 @@
 {% extends 'generic/object.html' %}
 
 {% block title%}
-Extra Config for {{ object.assigned_object }} 
+Extra Config Structure for {{ object.assigned_object }} 
 {% endblock title%}
 
 {% block content %}
@@ -16,7 +16,7 @@ Extra Config for {{ object.assigned_object }}
             <td>{{ object.extra_config_name }}</td>
           </tr>
           <tr>
-            <th scope="row">Extra Config</th>
+            <th scope="row">Extra Config Structure</th>
             {% load extras %}
             <td><pre class="block">{{ object.extra_config_structure | pretty_json }}</pre></td>
           </tr>
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/vm_vmassignedextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/vm_vmassignedextraconfig.html
new file mode 100644
index 0000000..3c9a7f7
--- /dev/null
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/vm_vmassignedextraconfig.html
@@ -0,0 +1,41 @@
+{% block content %}
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">Extra Config</h5>
+        <div class="card-body">
+          <table class="table table-responsive">
+              <th scope="row">ID</th>
+              <th scope="row">Extra Config</th>
+              <th scope="row">Description</th>
+              {% for vmassignextraconfig in vmassignextraconfiginfo %}
+        <tr>
+            <td>{{ vmassignextraconfig.id }}</td>
+            <td>
+              {% if vmassignextraconfig.extra_config_values %}
+                <a href="{{ vmassignextraconfig.get_absolute_url }}">{{ vmassignextraconfig.extra_config_values }}</a>
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+            <td>
+              {% if vmassignextraconfig.provider_type_extra_config_assignment_desc %}
+                {{ vmassignextraconfig.provider_type_extra_config_assignment_desc }}
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+        </tr>
+        {% empty %}
+        <tr>
+            <td colspan="3">No Virtual Machine Extra Configs Found</td>
+        </tr>
+        {% endfor %}
+            </tr>
+          </table>
+        </div>
+      </div>
+      {% include 'inc/panels/custom_fields.html' %}
+    </div>
+  </div>
+{% endblock content %}
\ No newline at end of file
-- 
GitLab


From ba8d4ce95fbce1b62fc0879783d340067f3e4c09 Mon Sep 17 00:00:00 2001
From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu>
Date: Tue, 31 Dec 2024 15:07:05 +0000
Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=A6=BA=20Add=20extra=20values=20vs=20?=
 =?UTF-8?q?extra=20structure=20validation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 netbox_sys_plugin/forms/machine.py            | 31 +++++++++++++-
 netbox_sys_plugin/tables.py                   |  6 +--
 .../clustertype_providertypeextraconfig.html  |  8 ++--
 .../vmassignedextraconfig.html                |  8 +++-
 netbox_sys_plugin/utils.py                    | 42 ++++++++++++++++++-
 5 files changed, 84 insertions(+), 11 deletions(-)

diff --git a/netbox_sys_plugin/forms/machine.py b/netbox_sys_plugin/forms/machine.py
index 4d8c21b..7f00170 100644
--- a/netbox_sys_plugin/forms/machine.py
+++ b/netbox_sys_plugin/forms/machine.py
@@ -11,6 +11,7 @@ from netbox.forms import (
     NetBoxModelFilterSetForm,
 )
 from utilities.forms.fields import DynamicModelChoiceField, JSONField
+from . . utils import validate_extra_config_values
 from ..models import VmAssignedVirtualMachineType, VirtualMachineMaintenance, DomainNames, VmAssignedExtraConfig, ProviderTypeExtraConfig
 
 class VmAssignedVmTypeForm(NetBoxModelForm):
@@ -226,7 +227,7 @@ class DomainNamesFilterForm(NetBoxModelFilterSetForm):
 
 class VmAssignedExtraConfigForm(NetBoxModelForm):
     """
-    GUI form to add or edit a Virtual Machine Type.
+    GUI form to add or edit a Extra Config Assignment.
     """
     virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.none(), required=True, label="Virtual Machine"
@@ -241,6 +242,11 @@ class VmAssignedExtraConfigForm(NetBoxModelForm):
         }
     )
 
+    extra_config_values = JSONField(
+        label=_('Extra Config Values'),
+        required=False
+    )
+
     def __init__(self, *args, **kwargs):
         # Initialize helper selectors
         instance = kwargs.get("instance")
@@ -266,6 +272,29 @@ class VmAssignedExtraConfigForm(NetBoxModelForm):
         model = VmAssignedExtraConfig
         fields = ('virtual_machine','provider_type_extra_config','extra_config_values','provider_type_extra_config_assignment_desc','tags')
 
+    def clean_extra_config_values(self):
+        """
+        Validates `extra_config_values` against the selected `provider_type_extra_config`.
+        """
+        # Fetch the selected provider configuration
+        provider_config = self.cleaned_data.get("provider_type_extra_config")
+        extra_config_values = self.cleaned_data.get("extra_config_values", {})
+
+        # Validate against the structure if a provider config is selected
+        if provider_config and provider_config.extra_config_structure:
+            structure_dict = provider_config.extra_config_structure
+
+            first_structure_name = next(iter(structure_dict), None)
+            if first_structure_name:
+                structure = structure_dict[first_structure_name][0]
+
+                if isinstance(structure, dict):  # Ensure structure is in the expected format
+                    is_valid, errors = validate_extra_config_values(structure, extra_config_values)
+                    if not is_valid:
+                        raise ValidationError(errors)
+
+        return extra_config_values
+
     def clean(self):
         """
         Validates form inputs before submitting:
diff --git a/netbox_sys_plugin/tables.py b/netbox_sys_plugin/tables.py
index 02dcb4c..d0028ee 100644
--- a/netbox_sys_plugin/tables.py
+++ b/netbox_sys_plugin/tables.py
@@ -254,16 +254,16 @@ class VmAssignedExtraConfig(NetBoxTable):
     )
     
     assigned_object = tables.Column(
-        linkify=False,
+        linkify=True,
         orderable=True,
         verbose_name="Virtual Machine",
     )
     provider_type_extra_config = tables.Column(
-        linkify=False,
+        linkify=True,
         verbose_name="Extra Configuration Structure",
     )
     provider_type_extra_config_assignment_desc = tables.Column(
-        linkify=False,
+        linkify=True,
         verbose_name="Description",
     )
 
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html
index 0bc024e..64bbc40 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/clustertype_providertypeextraconfig.html
@@ -2,16 +2,16 @@
   <div class="row mb-3">
     <div class="col col-md-12">
       <div class="card">
-        <h5 class="card-header">Virtual Machine Types</h5>
+        <h5 class="card-header">Extra Config Structures</h5>
         <div class="card-body">
           <table class="table table-responsive">
-              <th scope="row">Extra Config</th>
+              <th scope="row">Extra Config Struture</th>
               <th scope="row">Description</th>
               {% for extraconfig in extraconfiginfo %}
         <tr>
             <td>
-              {% if extraconfig.extra_config %}
-                <a href="{{ extraconfig.get_absolute_url }}">{{ extraconfig.extra_config }}</a>
+              {% if extraconfig.extra_config_structure %}
+                <a href="{{ extraconfig.get_absolute_url }}">{{ extraconfig.extra_config_structure }}</a>
               {% else %}
                 {{ ''|placeholder }}
               {% endif %}
diff --git a/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html b/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html
index 8747cf0..cabd698 100644
--- a/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html
+++ b/netbox_sys_plugin/templates/netbox_sys_plugin/vmassignedextraconfig.html
@@ -12,7 +12,7 @@ Extra Config for {{ object.assigned_object }}
       <div class="card-body">
         <table class="table table-hover attr-table">
           <tr>
-            <th scope="row">Name</th>
+            <th scope="row">Structure Name</th>
             <td>
               {% if object.provider_type_extra_config %}
                 <a href="{{ object.provider_type_extra_config.get_absolute_url }}">{{ object.provider_type_extra_config }}</a>
@@ -21,7 +21,11 @@ Extra Config for {{ object.assigned_object }}
               {% endif %}
             </td>
           </tr>
-
+          <tr>
+            <th scope="row">Extra Config Values</th>
+            {% load extras %}
+            <td><pre class="block">{{ object.extra_config_values | pretty_json }}</pre></td>
+          </tr>
           <tr>
             <th scope="row">Description</th>
             <td>{{ object.provider_type_extra_config_assignment_desc }}</td>
diff --git a/netbox_sys_plugin/utils.py b/netbox_sys_plugin/utils.py
index 82c3136..e037a0a 100644
--- a/netbox_sys_plugin/utils.py
+++ b/netbox_sys_plugin/utils.py
@@ -28,4 +28,44 @@ def send_webhook(payload):
         response.raise_for_status()
         print(f"Webhook sent successfully: {response.status_code}")
     except requests.RequestException as e:
-        print(f"Error sending webhook: {e}")
\ No newline at end of file
+        print(f"Error sending webhook: {e}")
+
+
+def validate_extra_config_values(structure, values):
+    """Validates the user-provided `extra_config_values` against the `extra_config_structure`."""
+    errors = []
+
+    # Loop through each field in the structure
+    for field_name, field_props in structure.items():
+        field_type = field_props.get("type", "String").lower()
+        is_required = field_props.get("required", "false").lower() == "true"
+
+        # Check for missing required fields
+        if is_required:
+            if field_name not in values or values[field_name] in [None, ""]: 
+                errors.append(f"Missing or empty required field: '{field_name}' with type {field_type}")
+                continue
+
+        # Validate type for present fields
+        if field_name in values:
+            value = values[field_name]
+            if not is_valid_type(value, field_type):
+                errors.append(f"Incorrect type for field '{field_name}': "
+                              f"expected {field_type}, got {type(value).__name__}")
+
+    return len(errors) == 0, errors
+
+
+def is_valid_type(value, expected_type):
+    """Validates a value against the expected type."""
+    if expected_type == "string":
+        return isinstance(value, str)
+    elif expected_type == "integer":
+        try:
+            int(value)  # Allow string representation of numbers
+            return True
+        except ValueError:
+            return False
+    elif expected_type == "boolean":
+        return value in [True, False, "true", "false", "True", "False"]
+    return False  # Default to invalid for unknown types
\ No newline at end of file
-- 
GitLab