Bläddra i källkod

Closes #17608: Adds L2VPN.status field (#18791)

Jason Novinger 11 månader sedan
förälder
incheckning
6bc9302ce5

+ 13 - 0
docs/models/vpn/l2vpn.md

@@ -33,6 +33,19 @@ The technology employed in forming and operating the L2VPN. Choices include:
 !!! note
     Designating the type as VPWS, EPL, EP-LAN, EP-TREE will limit the L2VPN instance to two terminations.
 
+### Status
+
+The operational status of the L2VPN. By default, the following statuses are available:
+
+* Active (default)
+* Planned
+* Faulty
+
+!!! tip "Custom L2VPN statuses"
+    Additional L2VPN statuses may be defined by setting `L2VPN.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
+
+!!! info "This field was introduced in NetBox v4.3."
+
 ### Identifier
 
 An optional numeric identifier. This can be used to track a pseudowire ID, for example.

+ 4 - 0
netbox/templates/vpn/l2vpn.html

@@ -22,6 +22,10 @@
           <th scope="row">{% trans "Type" %}</th>
           <td>{{ object.get_type_display }}</td>
         </tr>
+        <tr>
+          <th scope="row">{% trans "Status" %}</th>
+          <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
+        </tr>
         <tr>
           <th scope="row">{% trans "Description" %}</th>
           <td>{{ object.description|placeholder }}</td>

+ 1 - 1
netbox/vpn/api/serializers_/l2vpn.py

@@ -38,7 +38,7 @@ class L2VPNSerializer(NetBoxModelSerializer):
     class Meta:
         model = L2VPN
         fields = [
-            'id', 'url', 'display_url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets',
+            'id', 'url', 'display_url', 'display', 'identifier', 'name', 'slug', 'type', 'status', 'import_targets',
             'export_targets', 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
         ]
         brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description')

+ 14 - 0
netbox/vpn/choices.py

@@ -267,3 +267,17 @@ class L2VPNTypeChoices(ChoiceSet):
         TYPE_EPLAN,
         TYPE_EPTREE
     )
+
+
+class L2VPNStatusChoices(ChoiceSet):
+    key = 'L2VPN.status'
+
+    STATUS_ACTIVE = 'active'
+    STATUS_PLANNED = 'planned'
+    STATUS_DECOMMISSIONING = 'decommissioning'
+
+    CHOICES = [
+        (STATUS_ACTIVE, _('Active'), 'green'),
+        (STATUS_PLANNED, _('Planned'), 'cyan'),
+        (STATUS_DECOMMISSIONING, _('Decommissioning'), 'red'),
+    ]

+ 4 - 1
netbox/vpn/filtersets.py

@@ -298,6 +298,9 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
         choices=L2VPNTypeChoices,
         null_value=None
     )
+    status = django_filters.MultipleChoiceFilter(
+        choices=L2VPNStatusChoices,
+    )
     import_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets',
         queryset=RouteTarget.objects.all(),
@@ -323,7 +326,7 @@ class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 
     class Meta:
         model = L2VPN
-        fields = ('id', 'identifier', 'name', 'slug', 'type', 'description')
+        fields = ('id', 'identifier', 'name', 'slug', 'status', 'type', 'description')
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 5 - 1
netbox/vpn/forms/bulk_edit.py

@@ -260,6 +260,10 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
 
 
 class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
+    status = forms.ChoiceField(
+        label=_('Status'),
+        choices=L2VPNStatusChoices,
+    )
     type = forms.ChoiceField(
         label=_('Type'),
         choices=add_blank_choice(L2VPNTypeChoices),
@@ -279,7 +283,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
 
     model = L2VPN
     fieldsets = (
-        FieldSet('type', 'tenant', 'description'),
+        FieldSet('status', 'type', 'tenant', 'description'),
     )
     nullable_fields = ('tenant', 'description', 'comments')
 

+ 5 - 0
netbox/vpn/forms/bulk_import.py

@@ -260,6 +260,11 @@ class L2VPNImportForm(NetBoxModelImportForm):
         required=False,
         to_field_name='name',
     )
+    status = CSVChoiceField(
+        label=_('Status'),
+        choices=L2VPNStatusChoices,
+        help_text=_('Operational status')
+    )
     type = CSVChoiceField(
         label=_('Type'),
         choices=L2VPNTypeChoices,

+ 6 - 1
netbox/vpn/forms/filtersets.py

@@ -210,9 +210,14 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = L2VPN
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('type', 'import_target_id', 'export_target_id', name=_('Attributes')),
+        FieldSet('type', 'status', 'import_target_id', 'export_target_id', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
+    status = forms.MultipleChoiceField(
+        label=_('Status'),
+        choices=L2VPNStatusChoices,
+        required=False
+    )
     type = forms.ChoiceField(
         label=_('Type'),
         choices=add_blank_choice(L2VPNTypeChoices),

+ 3 - 3
netbox/vpn/forms/model_forms.py

@@ -409,7 +409,7 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        FieldSet('name', 'slug', 'type', 'identifier', 'description', 'tags', name=_('L2VPN')),
+        FieldSet('name', 'slug', 'type', 'status', 'identifier', 'description', 'tags', name=_('L2VPN')),
         FieldSet('import_targets', 'export_targets', name=_('Route Targets')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
@@ -417,8 +417,8 @@ class L2VPNForm(TenancyForm, NetBoxModelForm):
     class Meta:
         model = L2VPN
         fields = (
-            'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
-            'comments', 'tags'
+            'name', 'slug', 'type', 'status', 'identifier', 'import_targets', 'export_targets', 'tenant',
+            'description', 'comments', 'tags'
         )
 
 

+ 16 - 0
netbox/vpn/migrations/0008_add_l2vpn_status.py

@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('vpn', '0007_natural_ordering'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='l2vpn',
+            name='status',
+            field=models.CharField(default='active', max_length=50),
+        ),
+    ]

+ 11 - 2
netbox/vpn/models/l2vpn.py

@@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
 from core.models import ObjectType
 from netbox.models import NetBoxModel, PrimaryModel
 from netbox.models.features import ContactsMixin
-from vpn.choices import L2VPNTypeChoices
+from vpn.choices import L2VPNStatusChoices, L2VPNTypeChoices
 from vpn.constants import L2VPN_ASSIGNMENT_MODELS
 
 __all__ = (
@@ -33,6 +33,12 @@ class L2VPN(ContactsMixin, PrimaryModel):
         max_length=50,
         choices=L2VPNTypeChoices
     )
+    status = models.CharField(
+        verbose_name=_('status'),
+        max_length=50,
+        choices=L2VPNStatusChoices,
+        default=L2VPNStatusChoices.STATUS_ACTIVE,
+    )
     identifier = models.BigIntegerField(
         verbose_name=_('identifier'),
         null=True,
@@ -56,7 +62,7 @@ class L2VPN(ContactsMixin, PrimaryModel):
         null=True
     )
 
-    clone_fields = ('type',)
+    clone_fields = ('type', 'status')
 
     class Meta:
         ordering = ('name', 'identifier')
@@ -68,6 +74,9 @@ class L2VPN(ContactsMixin, PrimaryModel):
             return f'{self.name} ({self.identifier})'
         return f'{self.name}'
 
+    def get_status_color(self):
+        return L2VPNStatusChoices.colors.get(self.status)
+
     @cached_property
     def can_add_termination(self):
         if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2:

+ 1 - 1
netbox/vpn/search.py

@@ -79,4 +79,4 @@ class L2VPNIndex(SearchIndex):
         ('description', 500),
         ('comments', 5000),
     )
-    display_attrs = ('type', 'identifier', 'tenant', 'description')
+    display_attrs = ('type', 'status', 'identifier', 'tenant', 'description')

+ 6 - 3
netbox/vpn/tables/l2vpn.py

@@ -23,6 +23,9 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('Name'),
         linkify=True
     )
+    status = columns.ChoiceFieldColumn(
+        verbose_name=_('Status')
+    )
     import_targets = columns.TemplateColumn(
         verbose_name=_('Import Targets'),
         template_code=L2VPN_TARGETS,
@@ -43,10 +46,10 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = L2VPN
         fields = (
-            'pk', 'name', 'slug', 'identifier', 'type', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
-            'description', 'comments', 'tags', 'created', 'last_updated',
+            'pk', 'name', 'slug', 'status', 'identifier', 'type', 'import_targets', 'export_targets', 'tenant',
+            'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
         )
-        default_columns = ('pk', 'name', 'identifier', 'type', 'description')
+        default_columns = ('pk', 'name', 'status', 'identifier', 'type', 'description')
 
 
 class L2VPNTerminationTable(NetBoxTable):

+ 51 - 6
netbox/vpn/tests/test_api.py

@@ -1,4 +1,5 @@
 from django.urls import reverse
+from rest_framework import status
 
 from dcim.choices import InterfaceTypeChoices
 from dcim.models import Interface
@@ -527,19 +528,22 @@ class L2VPNTest(APIViewTestCases.APIViewTestCase):
             'name': 'L2VPN 4',
             'slug': 'l2vpn-4',
             'type': 'vxlan',
-            'identifier': 33343344
+            'identifier': 33343344,
+            'status': L2VPNStatusChoices.STATUS_ACTIVE,
         },
         {
             'name': 'L2VPN 5',
             'slug': 'l2vpn-5',
             'type': 'vxlan',
-            'identifier': 33343345
+            'identifier': 33343345,
+            'status': L2VPNStatusChoices.STATUS_PLANNED,
         },
         {
             'name': 'L2VPN 6',
             'slug': 'l2vpn-6',
             'type': 'vpws',
-            'identifier': 33343346
+            'identifier': 33343346,
+            'status': L2VPNStatusChoices.STATUS_DECOMMISSIONING,
         },
     ]
     bulk_update_data = {
@@ -550,12 +554,53 @@ class L2VPNTest(APIViewTestCases.APIViewTestCase):
     def setUpTestData(cls):
 
         l2vpns = (
-            L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
-            L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
-            L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'),  # No RD
+            L2VPN(
+                name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001,
+                status=L2VPNStatusChoices.STATUS_ACTIVE,
+            ),
+            L2VPN(
+                name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002,
+                status=L2VPNStatusChoices.STATUS_PLANNED,
+            ),
+            L2VPN(
+                name='L2VPN 3', slug='l2vpn-3', type='vpls',
+                status=L2VPNStatusChoices.STATUS_DECOMMISSIONING,
+            ),  # No RD
         )
         L2VPN.objects.bulk_create(l2vpns)
 
+    def test_status_filter(self):
+        url = reverse('vpn-api:l2vpn-list')
+
+        self.add_permissions('vpn.view_l2vpn')
+        response = self.client.get(url, **self.header)
+        response_data = response.json()
+
+        # all L2VPNs present with not filter
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response_data['count'], 3)
+
+        # 1 L2VPN present with active status filter
+        filter_url = f'{url}?status={L2VPNStatusChoices.STATUS_ACTIVE}'
+        response = self.client.get(filter_url, **self.header)
+        response_data = response.json()
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response_data['count'], 1)
+
+        # 2 L2VPNs present with active and planned status filter
+        filter_url = f'{filter_url}&status={L2VPNStatusChoices.STATUS_PLANNED}'
+        response = self.client.get(filter_url, **self.header)
+        response_data = response.json()
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response_data['count'], 2)
+
+        # 1 L2VPN present with decommissioning status filter
+        filter_url = f'{url}?status={L2VPNStatusChoices.STATUS_DECOMMISSIONING}'
+        response = self.client.get(filter_url, **self.header)
+        response_data = response.json()
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        self.assertEqual(response_data['count'], 1)
+
 
 class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
     model = L2VPNTermination

+ 12 - 0
netbox/vpn/tests/test_filtersets.py

@@ -769,6 +769,7 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
                 name='L2VPN 1',
                 slug='l2vpn-1',
                 type=L2VPNTypeChoices.TYPE_VXLAN,
+                status=L2VPNStatusChoices.STATUS_ACTIVE,
                 identifier=65001,
                 description='foobar1'
             ),
@@ -776,6 +777,7 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
                 name='L2VPN 2',
                 slug='l2vpn-2',
                 type=L2VPNTypeChoices.TYPE_VPWS,
+                status=L2VPNStatusChoices.STATUS_PLANNED,
                 identifier=65002,
                 description='foobar2'
             ),
@@ -783,6 +785,7 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
                 name='L2VPN 3',
                 slug='l2vpn-3',
                 type=L2VPNTypeChoices.TYPE_VPLS,
+                status=L2VPNStatusChoices.STATUS_DECOMMISSIONING,
                 description='foobar3'
             ),
         )
@@ -814,6 +817,15 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'type': [L2VPNTypeChoices.TYPE_VXLAN, L2VPNTypeChoices.TYPE_VPWS]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_status(self):
+        self.assertEqual(self.filterset({}, self.queryset).qs.count(), 3)
+
+        params = {'status': [L2VPNStatusChoices.STATUS_ACTIVE, L2VPNStatusChoices.STATUS_PLANNED]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+        params = {'status': [L2VPNStatusChoices.STATUS_DECOMMISSIONING]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
     def test_description(self):
         params = {'description': ['foobar1', 'foobar2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 17 - 6
netbox/vpn/tests/test_views.py

@@ -574,16 +574,25 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         RouteTarget.objects.bulk_create(rts)
 
         l2vpns = (
-            L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650001'),
-            L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650002'),
-            L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650003')
+            L2VPN(
+                name='L2VPN 1', slug='l2vpn-1', status=L2VPNStatusChoices.STATUS_ACTIVE,
+                type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650001'
+            ),
+            L2VPN(
+                name='L2VPN 2', slug='l2vpn-2', status=L2VPNStatusChoices.STATUS_DECOMMISSIONING,
+                type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650002'
+            ),
+            L2VPN(
+                name='L2VPN 3', slug='l2vpn-3', status=L2VPNStatusChoices.STATUS_PLANNED,
+                type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650003'
+            )
         )
         L2VPN.objects.bulk_create(l2vpns)
 
         cls.csv_data = (
-            'name,slug,type,identifier',
-            'L2VPN 5,l2vpn-5,vxlan,456',
-            'L2VPN 6,l2vpn-6,vxlan,444',
+            'name,status,slug,type,identifier',
+            'L2VPN 5,active,l2vpn-5,vxlan,456',
+            'L2VPN 6,planned,l2vpn-6,vxlan,444',
         )
 
         cls.csv_update_data = (
@@ -594,12 +603,14 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
         cls.bulk_edit_data = {
             'description': 'New Description',
+            'status': L2VPNStatusChoices.STATUS_DECOMMISSIONING,
         }
 
         cls.form_data = {
             'name': 'L2VPN 8',
             'slug': 'l2vpn-8',
             'type': L2VPNTypeChoices.TYPE_VXLAN,
+            'status': L2VPNStatusChoices.STATUS_PLANNED,
             'identifier': 123,
             'description': 'Description',
             'import_targets': [rts[0].pk],