#9045 #9046 - remove legacy fields from Provider (#10377)

* #9045 - remove legacy fields from Provider

* Add safegaurd for legacy data to migration

* 9045 remove fields from forms and tables

* Update unrelated tests to use ASN model instead of Provider

* Fix migrations collision

Co-authored-by: jeremystretch <jstretch@ns1.com>
This commit is contained in:
Arthur Hanson 2022-09-28 12:22:19 -07:00 committed by GitHub
parent c349e06346
commit 20e3fdc782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 104 additions and 141 deletions

View File

@ -31,7 +31,7 @@ class ProviderSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'id', 'url', 'display', 'name', 'slug', 'account',
'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
] ]

View File

@ -65,7 +65,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = Provider model = Provider
fields = ['id', 'name', 'slug', 'asn', 'account'] fields = ['id', 'name', 'slug', 'account']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -73,8 +73,6 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(account__icontains=value) | Q(account__icontains=value) |
Q(noc_contact__icontains=value) |
Q(admin_contact__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )

View File

@ -20,10 +20,6 @@ __all__ = (
class ProviderBulkEditForm(NetBoxModelBulkEditForm): class ProviderBulkEditForm(NetBoxModelBulkEditForm):
asn = forms.IntegerField(
required=False,
label='ASN (legacy)'
)
asns = DynamicModelMultipleChoiceField( asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(), queryset=ASN.objects.all(),
label=_('ASNs'), label=_('ASNs'),
@ -34,20 +30,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
label='Account number' label='Account number'
) )
portal_url = forms.URLField(
required=False,
label='Portal'
)
noc_contact = forms.CharField(
required=False,
widget=SmallTextarea,
label='NOC contact'
)
admin_contact = forms.CharField(
required=False,
widget=SmallTextarea,
label='Admin contact'
)
comments = CommentField( comments = CommentField(
widget=SmallTextarea, widget=SmallTextarea,
label='Comments' label='Comments'
@ -55,10 +37,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')), (None, ('asns', 'account', )),
) )
nullable_fields = ( nullable_fields = (
'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'asns', 'account', 'comments',
) )

View File

@ -18,7 +18,7 @@ class ProviderCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Provider model = Provider
fields = ( fields = (
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'name', 'slug', 'account', 'comments',
) )

View File

@ -30,29 +30,17 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Provider', ('name', 'slug', 'asn', 'asns', 'tags')), ('Provider', ('name', 'slug', 'asns', 'tags')),
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')), ('Support Info', ('account',)),
) )
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asns', 'comments', 'tags', 'name', 'slug', 'account', 'asns', 'comments', 'tags',
] ]
widgets = {
'noc_contact': SmallTextarea(
attrs={'rows': 5}
),
'admin_contact': SmallTextarea(
attrs={'rows': 5}
),
}
help_texts = { help_texts = {
'name': "Full name of the provider", 'name': "Full name of the provider",
'asn': "BGP autonomous system number (if applicable)",
'portal_url': "URL of the provider's customer support portal",
'noc_contact': "NOC email address and phone number",
'admin_contact': "Administrative contact email address and phone number",
} }

View File

@ -0,0 +1,59 @@
import os
from django.db import migrations
from django.db.utils import DataError
def check_legacy_data(apps, schema_editor):
"""
Abort the migration if any legacy provider fields still contain data.
"""
Provider = apps.get_model('circuits', 'Provider')
provider_count = Provider.objects.exclude(asn__isnull=True).count()
if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
raise DataError(
f"Unable to proceed with deleting asn field from Provider model: Found {provider_count} "
f"providers with legacy ASN data. Please ensure all legacy provider ASN data has been "
f"migrated to ASN objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA "
f"environment variable to bypass this safeguard and delete all legacy provider ASN data."
)
provider_count = Provider.objects.exclude(admin_contact='', noc_contact='', portal_url='').count()
if provider_count and 'NETBOX_DELETE_LEGACY_DATA' not in os.environ:
raise DataError(
f"Unable to proceed with deleting contact fields from Provider model: Found {provider_count} "
f"providers with legacy contact data. Please ensure all legacy provider contact data has been "
f"migrated to contact objects before proceeding. Or, set the NETBOX_DELETE_LEGACY_DATA "
f"environment variable to bypass this safeguard and delete all legacy provider contact data."
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0039_unique_constraints'),
]
operations = [
migrations.RunPython(
code=check_legacy_data,
reverse_code=migrations.RunPython.noop
),
migrations.RemoveField(
model_name='provider',
name='admin_contact',
),
migrations.RemoveField(
model_name='provider',
name='asn',
),
migrations.RemoveField(
model_name='provider',
name='noc_contact',
),
migrations.RemoveField(
model_name='provider',
name='portal_url',
),
]

View File

@ -24,12 +24,6 @@ class Provider(NetBoxModel):
max_length=100, max_length=100,
unique=True unique=True
) )
asn = ASNField(
blank=True,
null=True,
verbose_name='ASN',
help_text='32-bit autonomous system number'
)
asns = models.ManyToManyField( asns = models.ManyToManyField(
to='ipam.ASN', to='ipam.ASN',
related_name='providers', related_name='providers',
@ -40,18 +34,6 @@ class Provider(NetBoxModel):
blank=True, blank=True,
verbose_name='Account number' verbose_name='Account number'
) )
portal_url = models.URLField(
blank=True,
verbose_name='Portal URL'
)
noc_contact = models.TextField(
blank=True,
verbose_name='NOC contact'
)
admin_contact = models.TextField(
blank=True,
verbose_name='Admin contact'
)
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
@ -62,7 +44,7 @@ class Provider(NetBoxModel):
) )
clone_fields = ( clone_fields = (
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'account',
) )
class Meta: class Meta:

View File

@ -41,10 +41,10 @@ class ProviderTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Provider model = Provider
fields = ( fields = (
'pk', 'id', 'name', 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asn_count', 'pk', 'id', 'name', 'asns', 'account', 'asn_count',
'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated', 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') default_columns = ('pk', 'name', 'account', 'circuit_count')
class ProviderNetworkTable(NetBoxTable): class ProviderNetworkTable(NetBoxTable):

View File

@ -20,7 +20,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider model = Provider
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url'] brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
bulk_update_data = { bulk_update_data = {
'asn': 1234, 'account': '1234',
} }
@classmethod @classmethod

View File

@ -25,11 +25,11 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
ASN.objects.bulk_create(asns) ASN.objects.bulk_create(asns)
providers = ( providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'), Provider(name='Provider 1', slug='provider-1', account='1234'),
Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'), Provider(name='Provider 2', slug='provider-2', account='2345'),
Provider(name='Provider 3', slug='provider-3', asn=65003, account='3456'), Provider(name='Provider 3', slug='provider-3', account='3456'),
Provider(name='Provider 4', slug='provider-4', asn=65004, account='4567'), Provider(name='Provider 4', slug='provider-4', account='4567'),
Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'), Provider(name='Provider 5', slug='provider-5', account='5678'),
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
providers[0].asns.set([asns[0]]) providers[0].asns.set([asns[0]])
@ -82,10 +82,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['provider-1', 'provider-2']} params = {'slug': ['provider-1', 'provider-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_asn(self): # Legacy field
params = {'asn': ['65001', '65002']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_asn_id(self): # ASN object assignment def test_asn_id(self): # ASN object assignment
asns = ASN.objects.all()[:2] asns = ASN.objects.all()[:2]
params = {'asn_id': [asns[0].pk, asns[1].pk]} params = {'asn_id': [asns[0].pk, asns[1].pk]}

View File

@ -23,9 +23,9 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
ASN.objects.bulk_create(asns) ASN.objects.bulk_create(asns)
providers = ( providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2', asn=65002), Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3', asn=65003), Provider(name='Provider 3', slug='provider-3'),
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
providers[0].asns.set([asns[0], asns[1]]) providers[0].asns.set([asns[0], asns[1]])
@ -37,12 +37,8 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
cls.form_data = { cls.form_data = {
'name': 'Provider X', 'name': 'Provider X',
'slug': 'provider-x', 'slug': 'provider-x',
'asn': 65123,
'asns': [asns[6].pk, asns[7].pk], 'asns': [asns[6].pk, asns[7].pk],
'account': '1234', 'account': '1234',
'portal_url': 'http://example.com/portal',
'noc_contact': 'noc@example.com',
'admin_contact': 'admin@example.com',
'comments': 'Another provider', 'comments': 'Another provider',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -55,11 +51,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'asn': 65009,
'account': '5678', 'account': '5678',
'portal_url': 'http://example.com/portal2',
'noc_contact': 'noc2@example.com',
'admin_contact': 'admin2@example.com',
'comments': 'New comments', 'comments': 'New comments',
} }
@ -104,8 +96,8 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
def setUpTestData(cls): def setUpTestData(cls):
providers = ( providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2', asn=65002), Provider(name='Provider 2', slug='provider-2'),
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from circuits.models import Provider from ipam.models import ASN, RIR
from dcim.models import Site from dcim.models import Site
from extras.validators import CustomValidator from extras.validators import CustomValidator
@ -67,21 +67,25 @@ custom_validator = MyValidator()
class CustomValidatorTest(TestCase): class CustomValidatorTest(TestCase):
@override_settings(CUSTOM_VALIDATORS={'circuits.provider': [min_validator]}) @classmethod
def setUpTestData(cls):
RIR.objects.create(name='RIR 1', slug='rir-1')
@override_settings(CUSTOM_VALIDATORS={'ipam.asn': [min_validator]})
def test_configuration(self): def test_configuration(self):
self.assertIn('circuits.provider', settings.CUSTOM_VALIDATORS) self.assertIn('ipam.asn', settings.CUSTOM_VALIDATORS)
validator = settings.CUSTOM_VALIDATORS['circuits.provider'][0] validator = settings.CUSTOM_VALIDATORS['ipam.asn'][0]
self.assertIsInstance(validator, CustomValidator) self.assertIsInstance(validator, CustomValidator)
@override_settings(CUSTOM_VALIDATORS={'circuits.provider': [min_validator]}) @override_settings(CUSTOM_VALIDATORS={'ipam.asn': [min_validator]})
def test_min(self): def test_min(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
Provider(name='Provider 1', slug='provider-1', asn=1).clean() ASN(asn=1, rir=RIR.objects.first()).clean()
@override_settings(CUSTOM_VALIDATORS={'circuits.provider': [max_validator]}) @override_settings(CUSTOM_VALIDATORS={'ipam.asn': [max_validator]})
def test_max(self): def test_max(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
Provider(name='Provider 1', slug='provider-1', asn=65535).clean() ASN(asn=65535, rir=RIR.objects.first()).clean()
@override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_length_validator]}) @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_length_validator]})
def test_min_length(self): def test_min_length(self):

View File

@ -19,17 +19,6 @@
<h5 class="card-header">Provider</h5> <h5 class="card-header">Provider</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr>
<th scope="row">ASN</th>
<td>
{% if object.asn %}
<div class="float-end text-warning">
<i class="mdi mdi-alert" title="This field will be removed in a future release. Please migrate this data to ASN objects."></i>
</div>
{% endif %}
{{ object.asn|placeholder }}
</td>
</tr>
<tr> <tr>
<th scope="row">ASNs</th> <th scope="row">ASNs</th>
<td> <td>
@ -44,24 +33,6 @@
<th scope="row">Account</th> <th scope="row">Account</th>
<td>{{ object.account|placeholder }}</td> <td>{{ object.account|placeholder }}</td>
</tr> </tr>
<tr>
<th scope="row">Customer Portal</th>
<td>
{% if object.portal_url %}
<a href="{{ object.portal_url }}">{{ object.portal_url }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">NOC Contact</th>
<td>{{ object.noc_contact|markdown|placeholder }}</td>
</tr>
<tr>
<th scope="row">Admin Contact</th>
<td>{{ object.admin_contact|markdown|placeholder }}</td>
</tr>
<tr> <tr>
<th scope="row">Circuits</th> <th scope="row">Circuits</th>
<td> <td>

View File

@ -5,8 +5,6 @@ from django.test import TestCase
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet
from circuits.models import Circuit, Provider
from dcim.choices import * from dcim.choices import *
from dcim.fields import MACAddressField from dcim.fields import MACAddressField
from dcim.filtersets import DeviceFilterSet, SiteFilterSet from dcim.filtersets import DeviceFilterSet, SiteFilterSet
@ -15,6 +13,7 @@ from dcim.models import (
) )
from extras.filters import TagFilter from extras.filters import TagFilter
from extras.models import TaggedItem from extras.models import TaggedItem
from ipam.filtersets import ASNFilterSet
from ipam.models import RIR, ASN from ipam.models import RIR, ASN
from netbox.filtersets import BaseFilterSet from netbox.filtersets import BaseFilterSet
from utilities.filters import ( from utilities.filters import (
@ -338,13 +337,14 @@ class DynamicFilterLookupExpressionTest(TestCase):
""" """
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
rir = RIR.objects.create(name='RIR 1', slug='rir-1')
providers = ( asns = (
Provider(name='Provider 1', slug='provider-1', asn=65001), ASN(asn=65001, rir=rir),
Provider(name='Provider 2', slug='provider-2', asn=65101), ASN(asn=65101, rir=rir),
Provider(name='Provider 3', slug='provider-3', asn=65201), ASN(asn=65201, rir=rir),
) )
Provider.objects.bulk_create(providers) ASN.objects.bulk_create(asns)
manufacturers = ( manufacturers = (
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
@ -389,15 +389,6 @@ class DynamicFilterLookupExpressionTest(TestCase):
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = [
ASN(asn=65001, rir=rir),
ASN(asn=65101, rir=rir),
ASN(asn=65201, rir=rir)
]
ASN.objects.bulk_create(asns)
asns[0].sites.add(sites[0]) asns[0].sites.add(sites[0])
asns[1].sites.add(sites[1]) asns[1].sites.add(sites[1])
asns[2].sites.add(sites[2]) asns[2].sites.add(sites[2])
@ -456,19 +447,19 @@ class DynamicFilterLookupExpressionTest(TestCase):
def test_provider_asn_lt(self): def test_provider_asn_lt(self):
params = {'asn__lt': [65101]} params = {'asn__lt': [65101]}
self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 1) self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 1)
def test_provider_asn_lte(self): def test_provider_asn_lte(self):
params = {'asn__lte': [65101]} params = {'asn__lte': [65101]}
self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 2) self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 2)
def test_provider_asn_gt(self): def test_provider_asn_gt(self):
params = {'asn__lt': [65101]} params = {'asn__lt': [65101]}
self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 1) self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 1)
def test_provider_asn_gte(self): def test_provider_asn_gte(self):
params = {'asn__gte': [65101]} params = {'asn__gte': [65101]}
self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 2) self.assertEqual(ASNFilterSet(params, ASN.objects.all()).qs.count(), 2)
def test_site_region_negation(self): def test_site_region_negation(self):
params = {'region__n': ['region-1']} params = {'region__n': ['region-1']}