From 8274903985428c752d0f1d73b1a9b3d5e87392c7 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 29 Jan 2020 16:46:44 -0500 Subject: [PATCH 001/124] version bump for v2.8.0 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 8cdbb60a3..c9e62100c 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured # Environment setup # -VERSION = '2.7.4-dev' +VERSION = '2.8.0-dev' # Hostname HOSTNAME = platform.node() From 8cb6aed8fa47da3671864df63137fe0b3602eb28 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 13:59:07 -0500 Subject: [PATCH 002/124] Closes #3753: Remove rack units endpoint (replaced with elevation) --- docs/release-notes/version-2.8.md | 5 +++++ netbox/dcim/api/views.py | 27 --------------------------- netbox/dcim/tests/test_api.py | 7 ------- 3 files changed, 5 insertions(+), 34 deletions(-) create mode 100644 docs/release-notes/version-2.8.md diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md new file mode 100644 index 000000000..a29768189 --- /dev/null +++ b/docs/release-notes/version-2.8.md @@ -0,0 +1,5 @@ +# v2.8.0 (FUTURE) + +## API Changes + +* dcim.Rack: The `/api/dcim/racks//units/` endpoint has been replaced with `/api/dcim/racks//elevation/`. diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 8bb127f67..9e7935293 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -176,33 +176,6 @@ class RackViewSet(CustomFieldModelViewSet): serializer_class = serializers.RackSerializer filterset_class = filters.RackFilterSet - @swagger_auto_schema(deprecated=True) - @action(detail=True) - def units(self, request, pk=None): - """ - List rack units (by rack) - """ - # TODO: Remove this action detail route in v2.8 - rack = get_object_or_404(Rack, pk=pk) - face = request.GET.get('face', 'front') - exclude_pk = request.GET.get('exclude', None) - if exclude_pk is not None: - try: - exclude_pk = int(exclude_pk) - except ValueError: - exclude_pk = None - elevation = rack.get_rack_units(face, exclude_pk) - - # Enable filtering rack units by ID - q = request.GET.get('q', None) - if q: - elevation = [u for u in elevation if q in str(u['id'])] - - page = self.paginate_queryset(elevation) - if page is not None: - rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) - return self.get_paginated_response(rack_units.data) - @swagger_auto_schema( responses={200: serializers.RackUnitSerializer(many=True)}, query_serializer=serializers.RackElevationDetailFilterSerializer diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index a3a072bc9..c0ea5eca7 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -589,13 +589,6 @@ class RackTest(APITestCase): self.assertEqual(response.data['name'], self.rack1.name) - def test_get_rack_units(self): - - url = reverse('dcim-api:rack-units', kwargs={'pk': self.rack1.pk}) - response = self.client.get(url, **self.header) - - self.assertEqual(response.data['count'], 42) - def test_get_rack_elevation(self): url = reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}) From b475a575e43003143ae80f858995934a89319860 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 15:04:33 -0500 Subject: [PATCH 003/124] Drop family column from Aggregate, Prefix, and IPAddress models --- netbox/ipam/fields.py | 2 + netbox/ipam/filters.py | 6 +- netbox/ipam/lookups.py | 9 +++ netbox/ipam/managers.py | 2 +- netbox/ipam/migrations/0035_drop_ip_family.py | 38 ++++++++++++ netbox/ipam/models.py | 58 +++++++++---------- netbox/ipam/views.py | 2 +- netbox/templates/ipam/aggregate.html | 2 +- netbox/templates/ipam/ipaddress.html | 2 +- netbox/templates/ipam/prefix.html | 2 +- 10 files changed, 83 insertions(+), 40 deletions(-) create mode 100644 netbox/ipam/migrations/0035_drop_ip_family.py diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 456a7debc..7d28127a4 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -63,6 +63,7 @@ IPNetworkField.register_lookup(lookups.NetContained) IPNetworkField.register_lookup(lookups.NetContainedOrEqual) IPNetworkField.register_lookup(lookups.NetContains) IPNetworkField.register_lookup(lookups.NetContainsOrEquals) +IPNetworkField.register_lookup(lookups.NetFamily) IPNetworkField.register_lookup(lookups.NetMaskLength) @@ -90,4 +91,5 @@ IPAddressField.register_lookup(lookups.NetContainsOrEquals) IPAddressField.register_lookup(lookups.NetHost) IPAddressField.register_lookup(lookups.NetIn) IPAddressField.register_lookup(lookups.NetHostContained) +IPAddressField.register_lookup(lookups.NetFamily) IPAddressField.register_lookup(lookups.NetMaskLength) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 5f8bcabff..e773fed27 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -91,7 +91,7 @@ class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): class Meta: model = Aggregate - fields = ['family', 'date_added'] + fields = ('date_added',) def search(self, queryset, name, value): if not value.strip(): @@ -211,7 +211,7 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt class Meta: model = Prefix - fields = ['family', 'is_pool'] + fields = ('is_pool',) def search(self, queryset, name, value): if not value.strip(): @@ -350,7 +350,7 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF class Meta: model = IPAddress - fields = ['family', 'dns_name'] + fields = ('dns_name',) def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 62b356c5d..306e1e925 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -154,6 +154,15 @@ class NetHostContained(Lookup): return 'CAST(HOST(%s) AS INET) << %s' % (lhs, rhs), params +class NetFamily(Transform): + lookup_name = 'family' + function = 'FAMILY' + + @property + def output_field(self): + return IntegerField() + + class NetMaskLength(Transform): lookup_name = 'net_mask_length' function = 'MASKLEN' diff --git a/netbox/ipam/managers.py b/netbox/ipam/managers.py index 8aebc60ce..47dd08251 100644 --- a/netbox/ipam/managers.py +++ b/netbox/ipam/managers.py @@ -13,4 +13,4 @@ class IPAddressManager(models.Manager): IP address as a /32 or /128. """ qs = super().get_queryset() - return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host') + return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('host') diff --git a/netbox/ipam/migrations/0035_drop_ip_family.py b/netbox/ipam/migrations/0035_drop_ip_family.py new file mode 100644 index 000000000..e0142973f --- /dev/null +++ b/netbox/ipam/migrations/0035_drop_ip_family.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.9 on 2020-02-14 19:36 + +from django.db import migrations +import django.db.models.expressions + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0034_fix_ipaddress_status_dhcp'), + ] + + operations = [ + migrations.AlterModelOptions( + name='aggregate', + options={'ordering': ('prefix', 'pk')}, + ), + migrations.AlterModelOptions( + name='ipaddress', + options={'ordering': ('address', 'pk'), 'verbose_name': 'IP address', 'verbose_name_plural': 'IP addresses'}, + ), + migrations.AlterModelOptions( + name='prefix', + options={'ordering': (django.db.models.expressions.OrderBy(django.db.models.expressions.F('vrf'), nulls_first=True), 'prefix', 'pk'), 'verbose_name_plural': 'prefixes'}, + ), + migrations.RemoveField( + model_name='aggregate', + name='family', + ), + migrations.RemoveField( + model_name='ipaddress', + name='family', + ), + migrations.RemoveField( + model_name='prefix', + name='family', + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index b4ba92fb5..025c4c8af 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -150,9 +150,6 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize the hierarchy and track the overall utilization of available address space. Each Aggregate is assigned to a RIR. """ - family = models.PositiveSmallIntegerField( - choices=IPAddressFamilyChoices - ) prefix = IPNetworkField() rir = models.ForeignKey( to='ipam.RIR', @@ -182,7 +179,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): ] class Meta: - ordering = ('family', 'prefix', 'pk') # (family, prefix) may be non-unique + ordering = ('prefix', 'pk') # prefix may be non-unique def __str__(self): return str(self.prefix) @@ -225,12 +222,6 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): ) }) - def save(self, *args, **kwargs): - if self.prefix: - # Infer address family from IPNetwork object - self.family = self.prefix.version - super().save(*args, **kwargs) - def to_csv(self): return ( self.prefix, @@ -239,6 +230,12 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel): self.description, ) + @property + def family(self): + if self.prefix: + return self.prefix.version + return None + def get_utilization(self): """ Determine the prefix utilization of the aggregate and return it as a percentage. @@ -291,10 +288,6 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be assigned to a VLAN where appropriate. """ - family = models.PositiveSmallIntegerField( - choices=IPAddressFamilyChoices, - editable=False - ) prefix = IPNetworkField( help_text='IPv4 or IPv6 network with mask' ) @@ -376,7 +369,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): } class Meta: - ordering = (F('vrf').asc(nulls_first=True), 'family', 'prefix', 'pk') # (vrf, family, prefix) may be non-unique + ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique verbose_name_plural = 'prefixes' def __str__(self): @@ -423,9 +416,6 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): # Clear host bits from prefix self.prefix = self.prefix.cidr - # Record address family - self.family = self.prefix.version - super().save(*args, **kwargs) def to_csv(self): @@ -442,6 +432,12 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): self.description, ) + @property + def family(self): + if self.prefix: + return self.prefix.version + return None + def _set_prefix_length(self, value): """ Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly, @@ -501,9 +497,9 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): # All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable if ( - self.family == 4 and self.prefix.prefixlen == 31 # RFC 3021 + self.prefix.version == 4 and self.prefix.prefixlen == 31 # RFC 3021 ) or ( - self.family == 6 and self.prefix.prefixlen == 127 # RFC 6164 + self.prefix.version == 6 and self.prefix.prefixlen == 127 # RFC 6164 ): return available_ips @@ -546,7 +542,7 @@ class Prefix(ChangeLoggedModel, CustomFieldModel): # Compile an IPSet to avoid counting duplicate IPs child_count = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]).size prefix_size = self.prefix.size - if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: + if self.prefix.version == 4 and self.prefix.prefixlen < 31 and not self.is_pool: prefix_size -= 2 return int(float(child_count) / prefix_size * 100) @@ -562,10 +558,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP. """ - family = models.PositiveSmallIntegerField( - choices=IPAddressFamilyChoices, - editable=False - ) address = IPAddressField( help_text='IPv4 or IPv6 address (with mask)' ) @@ -659,7 +651,7 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): } class Meta: - ordering = ('family', 'address', 'pk') # (family, address) may be non-unique + ordering = ('address', 'pk') # address may be non-unique verbose_name = 'IP address' verbose_name_plural = 'IP addresses' @@ -727,10 +719,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): def save(self, *args, **kwargs): - # Record address family - if isinstance(self.address, netaddr.IPNetwork): - self.family = self.address.version - # Force dns_name to lowercase self.dns_name = self.dns_name.lower() @@ -754,9 +742,9 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): def to_csv(self): # Determine if this IP is primary for a Device - if self.family == 4 and getattr(self, 'primary_ip4_for', False): + if self.address.version == 4 and getattr(self, 'primary_ip4_for', False): is_primary = True - elif self.family == 6 and getattr(self, 'primary_ip6_for', False): + elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False): is_primary = True else: is_primary = False @@ -775,6 +763,12 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): self.description, ) + @property + def family(self): + if self.address: + return self.address.version + return None + def _set_mask_length(self, value): """ Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly, diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 053098f0b..faf349222 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -207,7 +207,7 @@ class RIRListView(PermissionRequiredMixin, ObjectListView): 'deprecated': 0, 'available': 0, } - aggregate_list = Aggregate.objects.filter(family=family, rir=rir) + aggregate_list = Aggregate.objects.filter(prefix__family=family, rir=rir) for aggregate in aggregate_list: queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix)) diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 66281aace..c34380722 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -64,7 +64,7 @@ - + diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 50bd90610..167d3fddf 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -65,7 +65,7 @@
Family{{ aggregate.get_family_display }}IPv{{ aggregate.family }}
RIR
- + diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 324bd927d..f14cab259 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -85,7 +85,7 @@
Family{{ ipaddress.get_family_display }}IPv{{ ipaddress.family }}
VRF
- + From 047f13ac5d47038568f49ea55d0861222a93e4b0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 15:07:59 -0500 Subject: [PATCH 004/124] Update tests --- netbox/dcim/tests/test_filters.py | 4 +- netbox/ipam/tests/test_filters.py | 52 +++++----- netbox/ipam/tests/test_models.py | 95 +++++++++--------- netbox/ipam/tests/test_ordering.py | 154 ++++++++++++++--------------- netbox/ipam/tests/test_views.py | 18 ++-- 5 files changed, 161 insertions(+), 162 deletions(-) diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index 83f40fe56..16c1ca58d 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1144,8 +1144,8 @@ class DeviceTestCase(TestCase): # Assign primary IPs for filtering ipaddresses = ( - IPAddress(family=4, address='192.0.2.1/24', interface=interfaces[0]), - IPAddress(family=4, address='192.0.2.2/24', interface=interfaces[1]), + IPAddress(address='192.0.2.1/24', interface=interfaces[0]), + IPAddress(address='192.0.2.2/24', interface=interfaces[1]), ) IPAddress.objects.bulk_create(ipaddresses) Device.objects.filter(pk=devices[0].pk).update(primary_ip4=ipaddresses[0]) diff --git a/netbox/ipam/tests/test_filters.py b/netbox/ipam/tests/test_filters.py index 4737a0f53..f523b1d20 100644 --- a/netbox/ipam/tests/test_filters.py +++ b/netbox/ipam/tests/test_filters.py @@ -96,12 +96,12 @@ class AggregateTestCase(TestCase): RIR.objects.bulk_create(rirs) aggregates = ( - Aggregate(family=4, prefix='10.1.0.0/16', rir=rirs[0], date_added='2020-01-01'), - Aggregate(family=4, prefix='10.2.0.0/16', rir=rirs[0], date_added='2020-01-02'), - Aggregate(family=4, prefix='10.3.0.0/16', rir=rirs[1], date_added='2020-01-03'), - Aggregate(family=6, prefix='2001:db8:1::/48', rir=rirs[1], date_added='2020-01-04'), - Aggregate(family=6, prefix='2001:db8:2::/48', rir=rirs[2], date_added='2020-01-05'), - Aggregate(family=6, prefix='2001:db8:3::/48', rir=rirs[2], date_added='2020-01-06'), + Aggregate(prefix='10.1.0.0/16', rir=rirs[0], date_added='2020-01-01'), + Aggregate(prefix='10.2.0.0/16', rir=rirs[0], date_added='2020-01-02'), + Aggregate(prefix='10.3.0.0/16', rir=rirs[1], date_added='2020-01-03'), + Aggregate(prefix='2001:db8:1::/48', rir=rirs[1], date_added='2020-01-04'), + Aggregate(prefix='2001:db8:2::/48', rir=rirs[2], date_added='2020-01-05'), + Aggregate(prefix='2001:db8:3::/48', rir=rirs[2], date_added='2020-01-06'), ) Aggregate.objects.bulk_create(aggregates) @@ -199,16 +199,16 @@ class PrefixTestCase(TestCase): Role.objects.bulk_create(roles) prefixes = ( - Prefix(family=4, prefix='10.0.0.0/24', site=None, vrf=None, vlan=None, role=None, is_pool=True), - Prefix(family=4, prefix='10.0.1.0/24', site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), - Prefix(family=4, prefix='10.0.2.0/24', site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), - Prefix(family=4, prefix='10.0.3.0/24', site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), - Prefix(family=6, prefix='2001:db8::/64', site=None, vrf=None, vlan=None, role=None, is_pool=True), - Prefix(family=6, prefix='2001:db8:0:1::/64', site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), - Prefix(family=6, prefix='2001:db8:0:2::/64', site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), - Prefix(family=6, prefix='2001:db8:0:3::/64', site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), - Prefix(family=4, prefix='10.0.0.0/16'), - Prefix(family=6, prefix='2001:db8::/32'), + Prefix(prefix='10.0.0.0/24', site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='10.0.1.0/24', site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), + Prefix(prefix='10.0.2.0/24', site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), + Prefix(prefix='10.0.3.0/24', site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), + Prefix(prefix='2001:db8::/64', site=None, vrf=None, vlan=None, role=None, is_pool=True), + Prefix(prefix='2001:db8:0:1::/64', site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), + Prefix(prefix='2001:db8:0:2::/64', site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), + Prefix(prefix='2001:db8:0:3::/64', site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), + Prefix(prefix='10.0.0.0/16'), + Prefix(prefix='2001:db8::/32'), ) Prefix.objects.bulk_create(prefixes) @@ -333,16 +333,16 @@ class IPAddressTestCase(TestCase): Interface.objects.bulk_create(interfaces) ipaddresses = ( - IPAddress(family=4, address='10.0.0.1/24', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), - IPAddress(family=4, address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), - IPAddress(family=4, address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), - IPAddress(family=4, address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), - IPAddress(family=4, address='10.0.0.1/25', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), - IPAddress(family=6, address='2001:db8::1/64', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), - IPAddress(family=6, address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), - IPAddress(family=6, address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), - IPAddress(family=6, address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), - IPAddress(family=6, address='2001:db8::1/65', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), + IPAddress(address='10.0.0.1/24', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), + IPAddress(address='10.0.0.2/24', vrf=vrfs[0], interface=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), + IPAddress(address='10.0.0.3/24', vrf=vrfs[1], interface=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), + IPAddress(address='10.0.0.4/24', vrf=vrfs[2], interface=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), + IPAddress(address='10.0.0.1/25', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), + IPAddress(address='2001:db8::1/64', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'), + IPAddress(address='2001:db8::2/64', vrf=vrfs[0], interface=interfaces[3], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'), + IPAddress(address='2001:db8::3/64', vrf=vrfs[1], interface=interfaces[4], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'), + IPAddress(address='2001:db8::4/64', vrf=vrfs[2], interface=interfaces[5], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'), + IPAddress(address='2001:db8::1/65', vrf=None, interface=None, status=IPAddressStatusChoices.STATUS_ACTIVE), ) IPAddress.objects.bulk_create(ipaddresses) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 235fae67f..6091aa70e 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -15,22 +15,22 @@ class TestAggregate(TestCase): # 25% utilization Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/12')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.16.0.0/12')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.32.0.0/12')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.48.0.0/12')), + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/12')), + Prefix(prefix=netaddr.IPNetwork('10.16.0.0/12')), + Prefix(prefix=netaddr.IPNetwork('10.32.0.0/12')), + Prefix(prefix=netaddr.IPNetwork('10.48.0.0/12')), )) self.assertEqual(aggregate.get_utilization(), 25) # 50% utilization Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.64.0.0/10')), + Prefix(prefix=netaddr.IPNetwork('10.64.0.0/10')), )) self.assertEqual(aggregate.get_utilization(), 50) # 100% utilization Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.128.0.0/9')), + Prefix(prefix=netaddr.IPNetwork('10.128.0.0/9')), )) self.assertEqual(aggregate.get_utilization(), 100) @@ -39,9 +39,9 @@ class TestPrefix(TestCase): def test_get_duplicates(self): prefixes = Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('192.0.2.0/24')), - Prefix(family=4, prefix=netaddr.IPNetwork('192.0.2.0/24')), - Prefix(family=4, prefix=netaddr.IPNetwork('192.0.2.0/24')), + Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')), + Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')), + Prefix(prefix=netaddr.IPNetwork('192.0.2.0/24')), )) duplicate_prefix_pks = [p.pk for p in prefixes[0].get_duplicates()] @@ -54,11 +54,11 @@ class TestPrefix(TestCase): VRF(name='VRF 3'), )) prefixes = Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/24'), vrf=None), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.1.0/24'), vrf=vrfs[0]), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.2.0/24'), vrf=vrfs[1]), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.3.0/24'), vrf=vrfs[2]), + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER), + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/24'), vrf=None), + Prefix(prefix=netaddr.IPNetwork('10.0.1.0/24'), vrf=vrfs[0]), + Prefix(prefix=netaddr.IPNetwork('10.0.2.0/24'), vrf=vrfs[1]), + Prefix(prefix=netaddr.IPNetwork('10.0.3.0/24'), vrf=vrfs[2]), )) child_prefix_pks = {p.pk for p in prefixes[0].get_child_prefixes()} @@ -79,13 +79,13 @@ class TestPrefix(TestCase): VRF(name='VRF 3'), )) parent_prefix = Prefix.objects.create( - family=4, prefix=netaddr.IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER + prefix=netaddr.IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER ) ips = IPAddress.objects.bulk_create(( - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.1/24'), vrf=None), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.1.1/24'), vrf=vrfs[0]), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.2.1/24'), vrf=vrfs[1]), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.3.1/24'), vrf=vrfs[2]), + IPAddress(address=netaddr.IPNetwork('10.0.0.1/24'), vrf=None), + IPAddress(address=netaddr.IPNetwork('10.0.1.1/24'), vrf=vrfs[0]), + IPAddress(address=netaddr.IPNetwork('10.0.2.1/24'), vrf=vrfs[1]), + IPAddress(address=netaddr.IPNetwork('10.0.3.1/24'), vrf=vrfs[2]), )) child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()} @@ -102,10 +102,10 @@ class TestPrefix(TestCase): def test_get_available_prefixes(self): prefixes = Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')), # Parent prefix - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/20')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.32.0/20')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.128.0/18')), + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/16')), # Parent prefix + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/20')), + Prefix(prefix=netaddr.IPNetwork('10.0.32.0/20')), + Prefix(prefix=netaddr.IPNetwork('10.0.128.0/18')), )) missing_prefixes = netaddr.IPSet([ netaddr.IPNetwork('10.0.16.0/20'), @@ -119,15 +119,15 @@ class TestPrefix(TestCase): def test_get_available_ips(self): - parent_prefix = Prefix.objects.create(family=4, prefix=netaddr.IPNetwork('10.0.0.0/28')) + parent_prefix = Prefix.objects.create(prefix=netaddr.IPNetwork('10.0.0.0/28')) IPAddress.objects.bulk_create(( - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.1/26')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.3/26')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.5/26')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.7/26')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.9/26')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.11/26')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.13/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.1/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.3/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.5/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.7/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.9/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.11/26')), + IPAddress(address=netaddr.IPNetwork('10.0.0.13/26')), )) missing_ips = netaddr.IPSet([ '10.0.0.2/32', @@ -145,40 +145,39 @@ class TestPrefix(TestCase): def test_get_first_available_prefix(self): prefixes = Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')), # Parent prefix - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.2.0/24')), + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/16')), # Parent prefix + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/24')), + Prefix(prefix=netaddr.IPNetwork('10.0.1.0/24')), + Prefix(prefix=netaddr.IPNetwork('10.0.2.0/24')), )) self.assertEqual(prefixes[0].get_first_available_prefix(), netaddr.IPNetwork('10.0.3.0/24')) - Prefix.objects.create(family=4, prefix=netaddr.IPNetwork('10.0.3.0/24')) + Prefix.objects.create(prefix=netaddr.IPNetwork('10.0.3.0/24')) self.assertEqual(prefixes[0].get_first_available_prefix(), netaddr.IPNetwork('10.0.4.0/22')) def test_get_first_available_ip(self): - parent_prefix = Prefix.objects.create(family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')) + parent_prefix = Prefix.objects.create(prefix=netaddr.IPNetwork('10.0.0.0/24')) IPAddress.objects.bulk_create(( - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.1/24')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.2/24')), - IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.3/24')), + IPAddress(address=netaddr.IPNetwork('10.0.0.1/24')), + IPAddress(address=netaddr.IPNetwork('10.0.0.2/24')), + IPAddress(address=netaddr.IPNetwork('10.0.0.3/24')), )) self.assertEqual(parent_prefix.get_first_available_ip(), '10.0.0.4/24') - IPAddress.objects.create(family=4, address=netaddr.IPNetwork('10.0.0.4/24')) + IPAddress.objects.create(address=netaddr.IPNetwork('10.0.0.4/24')) self.assertEqual(parent_prefix.get_first_available_ip(), '10.0.0.5/24') def test_get_utilization(self): # Container Prefix prefix = Prefix.objects.create( - family=4, prefix=netaddr.IPNetwork('10.0.0.0/24'), status=PrefixStatusChoices.STATUS_CONTAINER ) Prefix.objects.bulk_create(( - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.0/26')), - Prefix(family=4, prefix=netaddr.IPNetwork('10.0.0.128/26')), + Prefix(prefix=netaddr.IPNetwork('10.0.0.0/26')), + Prefix(prefix=netaddr.IPNetwork('10.0.0.128/26')), )) self.assertEqual(prefix.get_utilization(), 50) @@ -187,7 +186,7 @@ class TestPrefix(TestCase): prefix.save() IPAddress.objects.bulk_create( # Create 32 IPAddresses within the Prefix - [IPAddress(family=4, address=netaddr.IPNetwork('10.0.0.{}/24'.format(i))) for i in range(1, 33)] + [IPAddress(address=netaddr.IPNetwork('10.0.0.{}/24'.format(i))) for i in range(1, 33)] ) self.assertEqual(prefix.get_utilization(), 12) # ~= 12% @@ -224,9 +223,9 @@ class TestIPAddress(TestCase): def test_get_duplicates(self): ips = IPAddress.objects.bulk_create(( - IPAddress(family=4, address=netaddr.IPNetwork('192.0.2.1/24')), - IPAddress(family=4, address=netaddr.IPNetwork('192.0.2.1/24')), - IPAddress(family=4, address=netaddr.IPNetwork('192.0.2.1/24')), + IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')), + IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')), + IPAddress(address=netaddr.IPNetwork('192.0.2.1/24')), )) duplicate_ip_pks = [p.pk for p in ips[0].get_duplicates()] diff --git a/netbox/ipam/tests/test_ordering.py b/netbox/ipam/tests/test_ordering.py index 153bedddc..690501e53 100644 --- a/netbox/ipam/tests/test_ordering.py +++ b/netbox/ipam/tests/test_ordering.py @@ -42,45 +42,45 @@ class PrefixOrderingTestCase(OrderingTestBase): # Setup Prefixes prefixes = ( - Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.2.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.3.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.4.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.5.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.2.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.3.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.4.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.5.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/8')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.2.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.3.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.4.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.2.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.3.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.4.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.2.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.3.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.4.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.0.0/8')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.0.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.2.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.3.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.4.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.2.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.3.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.4.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.2.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.2.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.2.2.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.2.3.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.2.4.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/12')), - Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.2.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.3.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.4.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.0.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.2.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.3.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.4.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.0.0/12')), + Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.0.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.2.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.3.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.16.4.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, prefix=netaddr.IPNetwork('172.17.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.17.0.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.17.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.17.2.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.17.3.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, prefix=netaddr.IPNetwork('172.17.4.0/24')), ) Prefix.objects.bulk_create(prefixes) @@ -109,15 +109,15 @@ class PrefixOrderingTestCase(OrderingTestBase): # Setup Prefixes prefixes = [ - Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('10.0.0.0/8')), - Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('10.1.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/16')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/25')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.0.0/24')), - Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/8')), + Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('10.1.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/16')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.0.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.1.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.0.1.0/25')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.0.0/24')), + Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, prefix=netaddr.IPNetwork('10.1.1.0/24')), ] Prefix.objects.bulk_create(prefixes) @@ -136,39 +136,39 @@ class IPAddressOrderingTestCase(OrderingTestBase): # Setup Addresses addresses = ( - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.0.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.1.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.2.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.3.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.4.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.0.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.1.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.2.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.3.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.4.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.0.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.1.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.2.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.3.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.4.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.0.0.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.0.1.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.0.2.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.0.3.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.0.4.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.1.0.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.1.1.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.1.2.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.1.3.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.1.4.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.2.0.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.2.1.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.2.2.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.2.3.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, address=netaddr.IPNetwork('10.2.4.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.0.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.1.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.2.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.3.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.4.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.0.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.1.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.2.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.3.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.4.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.16.0.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.16.1.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.16.2.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.16.3.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.16.4.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.17.0.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.17.1.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.17.2.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.17.3.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, address=netaddr.IPNetwork('172.17.4.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.0.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.1.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.2.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.3.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.4.1/24')), - IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.5.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, address=netaddr.IPNetwork('192.168.0.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, address=netaddr.IPNetwork('192.168.1.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, address=netaddr.IPNetwork('192.168.2.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, address=netaddr.IPNetwork('192.168.3.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, address=netaddr.IPNetwork('192.168.4.1/24')), + IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, address=netaddr.IPNetwork('192.168.5.1/24')), ) IPAddress.objects.bulk_create(addresses) diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 66e649005..82ae18faf 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -82,9 +82,9 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase): RIR.objects.bulk_create(rirs) Aggregate.objects.bulk_create([ - Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rirs[0]), - Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rirs[0]), - Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]), + Aggregate(prefix=IPNetwork('10.1.0.0/16'), rir=rirs[0]), + Aggregate(prefix=IPNetwork('10.2.0.0/16'), rir=rirs[0]), + Aggregate(prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]), ]) cls.form_data = { @@ -161,9 +161,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) Prefix.objects.bulk_create([ - Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), - Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), - Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), + Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), + Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), + Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), ]) cls.form_data = { @@ -209,9 +209,9 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) IPAddress.objects.bulk_create([ - IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrfs[0]), - IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrfs[0]), - IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]), + IPAddress(address=IPNetwork('192.0.2.1/24'), vrf=vrfs[0]), + IPAddress(address=IPNetwork('192.0.2.2/24'), vrf=vrfs[0]), + IPAddress(address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]), ]) cls.form_data = { From 8687226cc74b2dfb2c1e06ef929f57e0a58b953d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 15:11:12 -0500 Subject: [PATCH 005/124] Update family filters in querysets --- netbox/dcim/forms.py | 4 ++-- netbox/virtualization/forms.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4c8a0821f..ad750404b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1857,14 +1857,14 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): # Collect interface IPs interface_ips = IPAddress.objects.prefetch_related('interface').filter( - family=family, interface_id__in=interface_ids + address__family=family, interface_id__in=interface_ids ) if interface_ips: ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips] ip_choices.append(('Interface IPs', ip_list)) # Collect NAT IPs nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( - family=family, nat_inside__interface__in=interface_ids + address__family=family, nat_inside__interface__in=interface_ids ) if nat_ips: ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips] diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 0dbe38324..d110545c7 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -408,7 +408,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ip_choices = [(None, '---------')] # Collect interface IPs interface_ips = IPAddress.objects.prefetch_related('interface').filter( - family=family, interface__virtual_machine=self.instance + address__family=family, interface__virtual_machine=self.instance ) if interface_ips: ip_choices.append( @@ -418,7 +418,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ) # Collect NAT IPs nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( - family=family, nat_inside__interface__virtual_machine=self.instance + address__family=family, nat_inside__interface__virtual_machine=self.instance ) if nat_ips: ip_choices.append( From fcdb05238c5ce46e9dac4fd12e640b4decc9d9ad Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 15:16:18 -0500 Subject: [PATCH 006/124] Restore filters --- netbox/ipam/filters.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index e773fed27..486a33a2e 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -73,6 +73,10 @@ class AggregateFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): method='search', label='Search', ) + family = django_filters.NumberFilter( + field_name='prefix', + lookup_expr='family' + ) prefix = django_filters.CharFilter( method='filter_prefix', label='Prefix', @@ -134,6 +138,10 @@ class PrefixFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilt method='search', label='Search', ) + family = django_filters.NumberFilter( + field_name='prefix', + lookup_expr='family' + ) prefix = django_filters.CharFilter( method='filter_prefix', label='Prefix', @@ -282,6 +290,10 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF method='search', label='Search', ) + family = django_filters.NumberFilter( + field_name='address', + lookup_expr='family' + ) parent = django_filters.CharFilter( method='search_by_parent', label='Parent prefix', From f0ced98dc6cbe25df11191fcceb6676ceb2e19c0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 15:17:04 -0500 Subject: [PATCH 007/124] Delete unused test data --- netbox/ipam/tests/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 82ae18faf..e168f02a4 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -88,7 +88,6 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase): ]) cls.form_data = { - 'family': 4, 'prefix': IPNetwork('10.99.0.0/16'), 'rir': rirs[1].pk, 'date_added': datetime.date(2020, 1, 1), From d6ccf131670c321e046b79ed40f68ea5a7c391c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Feb 2020 15:44:52 -0500 Subject: [PATCH 008/124] Changelog for #4081 --- docs/release-notes/version-2.8.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index a29768189..3088bb356 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -3,3 +3,7 @@ ## API Changes * dcim.Rack: The `/api/dcim/racks//units/` endpoint has been replaced with `/api/dcim/racks//elevation/`. + +## Other Changes + +* [#4081](https://github.com/netbox-community/netbox/issues/4081) - The `family` field has been removed from the Aggregate, Prefix, and IPAddress models From 01b9d1a4939bcf17cb2c37c6e65d619ffd1388f5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 18 Feb 2020 16:03:28 -0500 Subject: [PATCH 009/124] Closes #3848: Upgrade to Django 3.0 --- netbox/dcim/migrations/0097_mptt.py | 28 ++++++++++++++++++++++++++++ requirements.txt | 6 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 netbox/dcim/migrations/0097_mptt.py diff --git a/netbox/dcim/migrations/0097_mptt.py b/netbox/dcim/migrations/0097_mptt.py new file mode 100644 index 000000000..3011edb53 --- /dev/null +++ b/netbox/dcim/migrations/0097_mptt.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.3 on 2020-02-18 21:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0096_interface_ordering'), + ] + + operations = [ + migrations.AlterField( + model_name='region', + name='level', + field=models.PositiveIntegerField(editable=False), + ), + migrations.AlterField( + model_name='region', + name='lft', + field=models.PositiveIntegerField(editable=False), + ), + migrations.AlterField( + model_name='region', + name='rght', + field=models.PositiveIntegerField(editable=False), + ), + ] diff --git a/requirements.txt b/requirements.txt index b0b1b971d..c3b35e8fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -Django>=2.2,<2.3 +Django>=3.0,<3.1 django-cacheops==4.2 django-cors-headers==3.2.1 -django-debug-toolbar==2.1 +django-debug-toolbar==2.2 django-filter==2.2.0 -django-mptt==0.9.1 +django-mptt==0.11.0 django-pglocks==1.0.4 django-prometheus==1.1.0 django-rq==2.2.0 From f1b042180536092fa99d67f55fe5d5ab280d0f69 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 18 Feb 2020 18:00:00 -0500 Subject: [PATCH 010/124] Temporary hack to avoid name collision without renaming the secrets app --- netbox/secrets/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/netbox/secrets/__init__.py b/netbox/secrets/__init__.py index e69de29bb..3c1c4eed2 100644 --- a/netbox/secrets/__init__.py +++ b/netbox/secrets/__init__.py @@ -0,0 +1,11 @@ +# TODO: Rename the secrets app, probably +# Python 3.6 introduced a standard library named "secrets," which obviously conflicts with this Django app. To avoid +# renaming the app, we hotwire the components of the standard library that Django calls. (I don't like this any more +# than you do, but it works for now.) The only references to the secrets modules are in django/utils/crypto.py. +# +# First, we copy secrets.compare_digest, which comes from the hmac module: +from hmac import compare_digest + +# Then, we instantiate SystemRandom and map its choice() function: +from random import SystemRandom +choice = SystemRandom().choice From e17597a0a942ecf9d2f5d95f082a5a930c99d672 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 19 Feb 2020 14:30:49 -0500 Subject: [PATCH 011/124] Update CI build to Python 3.6 and PostgreSQL 9.6 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 872121c21..4ad37604e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,10 @@ services: - postgresql - redis-server addons: - postgresql: "9.4" + postgresql: "9.6" language: python python: - - "3.5" + - "3.6" install: - pip install -r requirements.txt - pip install pycodestyle From b7d41bc42c0ab5e1bf0ff89ec734eeb5d55052ea Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 19 Feb 2020 14:46:53 -0500 Subject: [PATCH 012/124] Rename MPTT migration --- .../dcim/migrations/{0097_mptt.py => 0097_mptt_remove_indexes.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename netbox/dcim/migrations/{0097_mptt.py => 0097_mptt_remove_indexes.py} (100%) diff --git a/netbox/dcim/migrations/0097_mptt.py b/netbox/dcim/migrations/0097_mptt_remove_indexes.py similarity index 100% rename from netbox/dcim/migrations/0097_mptt.py rename to netbox/dcim/migrations/0097_mptt_remove_indexes.py From 7b93155b06c79ccbbeed404b467d5d542f31c9a5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 19 Feb 2020 15:08:15 -0500 Subject: [PATCH 013/124] Fix form field ordering; self.fields no longer an OrderedDict --- netbox/extras/forms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index d6a5406b7..b792ec484 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -467,7 +467,8 @@ class ScriptForm(BootstrapMixin, forms.Form): self.fields['_commit'].initial = False # Move _commit to the end of the form - self.fields.move_to_end('_commit', True) + commit = self.fields.pop('_commit') + self.fields['_commit'] = commit @property def requires_input(self): From be232309382ca28fbf190aac285411984baf861f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 19 Feb 2020 15:31:15 -0500 Subject: [PATCH 014/124] Update tests to match new string representation of ContentTypes --- netbox/dcim/tests/test_api.py | 2 +- netbox/extras/tests/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index c0ea5eca7..607637d1e 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -40,7 +40,7 @@ class AppTest(APITestCase): self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict()) content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS) cable_termination_choices = { - "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types + "{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types } self.assertEqual(choices_to_dict(response.data.get('cable:termination_a_type')), cable_termination_choices) self.assertEqual(choices_to_dict(response.data.get('cable:termination_b_type')), cable_termination_choices) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index b15553a49..db2861f46 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -37,7 +37,7 @@ class AppTest(APITestCase): # Graph content_types = ContentType.objects.filter(GRAPH_MODELS) graph_type_choices = { - "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types + "{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types } self.assertEqual(choices_to_dict(response.data.get('graph:type')), graph_type_choices) self.assertEqual(choices_to_dict(response.data.get('graph:template_language')), TemplateLanguageChoices.as_dict()) From 5dc956fbe11718dff7f4b4b6284cdaef7ddc2b50 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Feb 2020 15:07:59 -0500 Subject: [PATCH 015/124] First stab at external authentication support --- docs/configuration/optional-settings.md | 48 +++++++++++++++++++++++++ netbox/netbox/configuration.example.py | 8 +++++ netbox/netbox/settings.py | 16 +++++++-- netbox/utilities/auth_backends.py | 47 +++++++++++++++++++++++- netbox/utilities/middleware.py | 9 +++++ 5 files changed, 124 insertions(+), 4 deletions(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index cbe01728c..5b4fd1d14 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -291,6 +291,54 @@ When determining the primary IP address for a device, IPv6 is preferred over IPv --- +## REMOTE_AUTH_ENABLED + +Default: `False` + +NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authenitcation will still take effect as a fallback.) + +--- + +## REMOTE_AUTH_BACKEND + +Default: `'utilities.auth_backends.RemoteUserBackend'` + +Python path to the custom [Django authentication backend]() to use for external user authentication, if not using NetBox's built-in backend. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + +## REMOTE_AUTH_HEADER + +Default: `'HTTP_REMOTE_USER'` + +When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + +## REMOTE_AUTH_AUTO_CREATE_USER + +Default: `True` + +If true, NetBox will automatically create local accounts for users authenticated via a remote service. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + +## REMOTE_AUTH_DEFAULT_GROUPS + +Default: `[]` (Empty list) + +The list of groups to assign a new user account when created using remote authentication. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + +## REMOTE_AUTH_DEFAULT_PERMISSIONS + +Default: `[]` (Empty list) + +The list of permissions to assign a new user account when created using remote authentication. (Requires `REMOTE_AUTH_ENABLED`.) + +--- + ## REPORTS_ROOT Default: $BASE_DIR/netbox/reports/ diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 7002def9b..1aada7c2d 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -179,6 +179,14 @@ PAGINATE_COUNT = 50 # prefer IPv4 instead. PREFER_IPV4 = False +# Remote authentication support +REMOTE_AUTH_ENABLED = False +REMOTE_AUTH_BACKEND = 'utilities.auth_backends.RemoteUserBackend' +REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER' +REMOTE_AUTH_AUTO_CREATE_USER = True +REMOTE_AUTH_DEFAULT_GROUPS = [] +REMOTE_AUTH_DEFAULT_PERMISSIONS = [] + # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of # this setting is derived from the installed location. # REPORTS_ROOT = '/opt/netbox/netbox/reports' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index bdd83723d..ed3974312 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -93,6 +93,12 @@ NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30) NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '') PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) +REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False) +REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'utilities.auth_backends.RemoteUserBackend') +REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', []) +REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', []) +REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False) +REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER') REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) @@ -258,7 +264,7 @@ INSTALLED_APPS = [ ] # Middleware -MIDDLEWARE = ( +MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django_prometheus.middleware.PrometheusBeforeMiddleware', 'corsheaders.middleware.CorsMiddleware', @@ -274,7 +280,9 @@ MIDDLEWARE = ( 'utilities.middleware.APIVersionMiddleware', 'extras.middleware.ObjectChangeMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware', -) +] +if REMOTE_AUTH_ENABLED: + MIDDLEWARE.append('utilities.middleware.RemoteUserMiddleware') ROOT_URLCONF = 'netbox.urls' @@ -297,10 +305,12 @@ TEMPLATES = [ }, ] -# Authentication +# Set up authentication backends AUTHENTICATION_BACKENDS = [ 'utilities.auth_backends.ViewExemptModelBackend', ] +if REMOTE_AUTH_ENABLED: + AUTHENTICATION_BACKENDS.insert(0, REMOTE_AUTH_BACKEND) # Internationalization LANGUAGE_CODE = 'en-us' diff --git a/netbox/utilities/auth_backends.py b/netbox/utilities/auth_backends.py index 54541b0b5..52c3454f1 100644 --- a/netbox/utilities/auth_backends.py +++ b/netbox/utilities/auth_backends.py @@ -1,5 +1,8 @@ +import logging + from django.conf import settings -from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as RemoteUserBackend_ +from django.contrib.auth.models import Group, Permission class ViewExemptModelBackend(ModelBackend): @@ -26,3 +29,45 @@ class ViewExemptModelBackend(ModelBackend): pass return super().has_perm(user_obj, perm, obj) + + +class RemoteUserBackend(ViewExemptModelBackend, RemoteUserBackend_): + """ + Custom implementation of Django's RemoteUserBackend which provides configuration hooks for basic customization. + """ + @property + def create_unknown_user(self): + return bool(settings.REMOTE_AUTH_AUTO_CREATE_USER) + + def configure_user(self, request, user): + logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + + # Assign default groups to the user + group_list = [] + for name in settings.REMOTE_AUTH_DEFAULT_GROUPS: + try: + group_list.append(Group.objects.get(name=name)) + except Group.DoesNotExist: + logging.error("Could not assign group {name} to remotely-authenticated user {user}: Group not found") + if group_list: + user.groups.add(*group_list) + logger.debug(f"Assigned groups to remotely-authenticated user {user}: {group_list}") + + # Assign default permissions to the user + permissions_list = [] + for permission_name in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS: + try: + app_label, codename = permission_name.split('.') + permissions_list.append( + Permission.objects.get(content_type__app_label=app_label, codename=codename) + ) + except (ValueError, Permission.DoesNotExist): + logging.error( + "Invalid permission name: '{permission_name}'. Permissions must be in the form " + "._. (Example: dcim.add_site)" + ) + if permissions_list: + user.user_permissions.add(*permissions_list) + logger.debug(f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}") + + return user diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index 564771821..a4fd1a254 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -1,6 +1,7 @@ from urllib import parse from django.conf import settings +from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_ from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect from django.urls import reverse @@ -30,6 +31,14 @@ class LoginRequiredMiddleware(object): return self.get_response(request) +class RemoteUserMiddleware(RemoteUserMiddleware_): + """ + Custom implementation of Django's RemoteUserMiddleware which allows for a user-configurable HTTP header name. + """ + force_logout_if_no_header = False + header = settings.REMOTE_AUTH_HEADER + + class APIVersionMiddleware(object): """ If the request is for an API endpoint, include the API version as a response header. From a17c22746d7cccdab70ef61ca67a9a3ede6aa8ab Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 29 Feb 2020 02:23:01 -0500 Subject: [PATCH 016/124] initial work on #3351 --- netbox/netbox/settings.py | 55 +++++++++++++++++++++++++++++++++++++++ netbox/netbox/urls.py | 18 +++++++++++++ 2 files changed, 73 insertions(+) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index bdd83723d..b65d0abe7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -1,3 +1,4 @@ +import importlib import logging import os import platform @@ -6,6 +7,7 @@ import warnings from django.contrib.messages import constants as messages from django.core.exceptions import ImproperlyConfigured +from pkg_resources import iter_entry_points # @@ -92,6 +94,7 @@ NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '') NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30) NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '') PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) +PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {}) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/') @@ -591,3 +594,55 @@ PER_PAGE_DEFAULTS = [ if PAGINATE_COUNT not in PER_PAGE_DEFAULTS: PER_PAGE_DEFAULTS.append(PAGINATE_COUNT) PER_PAGE_DEFAULTS = sorted(PER_PAGE_DEFAULTS) + + +# +# Plugins +# + +PLUGINS = [] +for entry_point in iter_entry_points(group='netbox.plugin', name=None): + plugin = entry_point.module_name + PLUGINS.append(plugin) + INSTALLED_APPS.append(plugin) + + try: + module = importlib.import_module(plugin) + default_app_config = getattr(module, 'default_app_config') + module, app_config = default_app_config.rsplit('.', 1) + app_config = getattr(importlib.import_module(module), app_config) + except ImportError: + raise ImproperlyConfigured('Plugin config for {} could not be imported!'.format(plugin)) + + app_config_meta = getattr(app_config, 'NetBoxPluginMeta', None) + if not app_config_meta: + raise ImproperlyConfigured( + 'The app config for plugin {} does not contain an inner meta class'.format(plugin) + ) + + # Add middleware + plugin_middleware = getattr(app_config_meta, 'middleware', []) + if plugin_middleware: + MIDDLEWARE.extend(plugin_middleware) + + # Add middleware + plugin_installed_apps = getattr(app_config_meta, 'installed_apps', []) + if plugin_installed_apps: + INSTALLED_APPS.extend(plugin_installed_apps) + + # Verify required configuration settings + if plugin not in PLUGINS_CONFIG: + PLUGINS_CONFIG[plugin] = {} + for setting in getattr(app_config_meta, 'required_settings', []): + if setting not in PLUGINS_CONFIG[plugin]: + raise ImproperlyConfigured( + "Plugin {} requires '{}' to be present in the PLUGINS_CONFIG section of configuration.py.".format( + plugin, + setting + ) + ) + + # Set defined default setting values + for setting, value in getattr(app_config_meta, 'default_settings', {}).items(): + if setting not in PLUGINS_CONFIG[plugin]: + PLUGINS_CONFIG[plugin][setting] = value diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 2c4d504b2..93bb84e06 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,3 +1,6 @@ +import importlib + +from django.apps import apps from django.conf import settings from django.conf.urls import include from django.urls import path, re_path @@ -65,6 +68,21 @@ _patterns = [ ] +# Plugins +plugin_patterns = [] +for app in apps.get_app_configs(): + if hasattr(app, 'NetBoxPluginMeta'): + if importlib.util.find_spec('{}.urls'.format(app.name)): + urls = importlib.import_module('{}.urls'.format(app.name)) + url_slug = getattr(app.NetBoxPluginMeta, 'url_slug', app.label) + plugin_patterns.append( + path('{}/'.format(url_slug), include((urls.urlpatterns, app.label))) + ) + +_patterns.append( + path('plugins/', include((plugin_patterns, 'plugins'))) +) + if settings.DEBUG: import debug_toolbar _patterns += [ From 71a8a13644ea4643202a626d7860ab6f6ac5351d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 1 Mar 2020 03:24:17 -0500 Subject: [PATCH 017/124] add api urls and signals interface for detail route buttons --- netbox/extras/plugins/__init__.py | 0 netbox/extras/plugins/signals.py | 34 +++++++++++++++++++ .../extras/plugins/templatetags/__init__.py | 0 netbox/extras/templatetags/plugins.py | 27 +++++++++++++++ netbox/netbox/settings.py | 12 +++---- netbox/netbox/urls.py | 19 +++++++++-- netbox/templates/dcim/device.html | 2 ++ netbox/templates/dcim/site.html | 2 ++ 8 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 netbox/extras/plugins/__init__.py create mode 100644 netbox/extras/plugins/signals.py create mode 100644 netbox/extras/plugins/templatetags/__init__.py create mode 100644 netbox/extras/templatetags/plugins.py diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/extras/plugins/signals.py b/netbox/extras/plugins/signals.py new file mode 100644 index 000000000..7d0567b1b --- /dev/null +++ b/netbox/extras/plugins/signals.py @@ -0,0 +1,34 @@ +import django.dispatch +from django.dispatch.dispatcher import NO_RECEIVERS + + +class PluginSignal(django.dispatch.Signal): + + def _sorted_receivers(self, sender): + orig_list = self._live_receivers(sender) + sorted_list = sorted( + orig_list, + key=lambda receiver: ( + receiver.__module__, + receiver.__name__, + ) + ) + return sorted_list + + def send(self, sender, **kwargs): + responses = [] + if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: + return responses + + for receiver in self._sorted_receivers(sender): + response = receiver(signal=self, sender=sender, **kwargs) + responses.append((receiver, response)) + return responses + + +""" +This signal collects templates which render buttons for object detail pages +""" +register_detail_page_buttons = PluginSignal( + providing_args=[] +) diff --git a/netbox/extras/plugins/templatetags/__init__.py b/netbox/extras/plugins/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/extras/templatetags/plugins.py b/netbox/extras/templatetags/plugins.py new file mode 100644 index 000000000..dc3443d0c --- /dev/null +++ b/netbox/extras/templatetags/plugins.py @@ -0,0 +1,27 @@ +from django import template as template_ +from django.template.loader import get_template +from django.utils.safestring import mark_safe + +from extras.plugins.signals import register_detail_page_buttons + + +register = template_.Library() + + +@register.simple_tag() +def plugin_buttons(obj): + """ + Fire signal to collect all buttons registered by plugins + """ + html = '' + responses = register_detail_page_buttons.send(obj) + for receiver, response in responses: + if not isinstance(response, list): + response = [response] + for template in response: + if isinstance(template, str): + template_text = get_template(template).render({'obj': obj}) + html += template_text + + return mark_safe(html) + diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b65d0abe7..8f1678b95 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -261,7 +261,7 @@ INSTALLED_APPS = [ ] # Middleware -MIDDLEWARE = ( +MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django_prometheus.middleware.PrometheusBeforeMiddleware', 'corsheaders.middleware.CorsMiddleware', @@ -277,7 +277,7 @@ MIDDLEWARE = ( 'utilities.middleware.APIVersionMiddleware', 'extras.middleware.ObjectChangeMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware', -) +] ROOT_URLCONF = 'netbox.urls' @@ -622,12 +622,12 @@ for entry_point in iter_entry_points(group='netbox.plugin', name=None): # Add middleware plugin_middleware = getattr(app_config_meta, 'middleware', []) - if plugin_middleware: + if plugin_middleware and isinstance(plugin_middleware, list): MIDDLEWARE.extend(plugin_middleware) - # Add middleware + # Add installed apps plugin_installed_apps = getattr(app_config_meta, 'installed_apps', []) - if plugin_installed_apps: + if plugin_installed_apps and isinstance(plugin_installed_apps, list): INSTALLED_APPS.extend(plugin_installed_apps) # Verify required configuration settings @@ -637,7 +637,7 @@ for entry_point in iter_entry_points(group='netbox.plugin', name=None): if setting not in PLUGINS_CONFIG[plugin]: raise ImproperlyConfigured( "Plugin {} requires '{}' to be present in the PLUGINS_CONFIG section of configuration.py.".format( - plugin, + plugin, setting ) ) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 93bb84e06..8d59a33d1 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -70,18 +70,31 @@ _patterns = [ # Plugins plugin_patterns = [] +plugin_api_patterns = [] for app in apps.get_app_configs(): if hasattr(app, 'NetBoxPluginMeta'): if importlib.util.find_spec('{}.urls'.format(app.name)): urls = importlib.import_module('{}.urls'.format(app.name)) url_slug = getattr(app.NetBoxPluginMeta, 'url_slug', app.label) - plugin_patterns.append( - path('{}/'.format(url_slug), include((urls.urlpatterns, app.label))) - ) + if hasattr(urls, 'urlpatterns'): + plugin_patterns.append( + path('{}/'.format(url_slug), include((urls.urlpatterns, app.label))) + ) + if importlib.util.find_spec('{}.api'.format(app.name)): + if importlib.util.find_spec('{}.api.urls'.format(app.name)): + urls = importlib.import_module('{}.api.urls'.format(app.name)) + if hasattr(urls, 'urlpatterns'): + url_slug = getattr(app.NetBoxPluginMeta, 'url_slug', app.label) + plugin_api_patterns.append( + path('{}/'.format(url_slug), include((urls.urlpatterns, app.label))) + ) _patterns.append( path('plugins/', include((plugin_patterns, 'plugins'))) ) +_patterns.append( + path('api/plugins/', include((plugin_api_patterns, 'plugins-api'))) +) if settings.DEBUG: import debug_toolbar diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 8c457121f..74e2549bd 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -3,6 +3,7 @@ {% load static %} {% load helpers %} {% load custom_links %} +{% load plugins %} {% block title %}{{ device }}{% endblock %} @@ -36,6 +37,7 @@
+ {% plugin_buttons device %} {% if show_graphs %}
+ {% plugin_buttons site %} {% if show_graphs %}
+ + +
Family{{ prefix.get_family_display }}IPv{{ prefix.family }}
VRF
Description + {{ tag.description }} +
-
-
- Comments -
-
- {% if tag.comments %} - {{ tag.comments|render_markdown }} - {% else %} - None - {% endif %} -
-
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %} diff --git a/netbox/templates/extras/tag_edit.html b/netbox/templates/extras/tag_edit.html index 800db1d26..87b9a2e53 100644 --- a/netbox/templates/extras/tag_edit.html +++ b/netbox/templates/extras/tag_edit.html @@ -8,12 +8,7 @@ {% render_field form.name %} {% render_field form.slug %} {% render_field form.color %} -
- -
-
Comments
-
- {% render_field form.comments %} + {% render_field form.description %}
{% endblock %} From 6ea15cec6f3917028d8226f5760a08be7c981bc3 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 15 Mar 2020 00:24:05 -0400 Subject: [PATCH 049/124] Revert "refactor extras registry" This reverts commit c189895f6c17bd64de21e419a2f6678342d62477. --- netbox/dcim/models/__init__.py | 4 +-- netbox/extras/api/serializers.py | 6 ++-- netbox/extras/models.py | 12 +++---- netbox/extras/tests/test_api.py | 3 +- netbox/extras/tests/test_filters.py | 5 +-- netbox/extras/utils.py | 54 +++++++++++++---------------- 6 files changed, 40 insertions(+), 44 deletions(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 40f022fa4..f702f8dff 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -21,7 +21,7 @@ from dcim.constants import * from dcim.fields import ASNField from dcim.elevations import RackElevationSVG from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem -from extras.utils import extras_features +from extras.utils import extras_functionality from utilities.fields import ColorField, NaturalOrderingField from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object, to_meters @@ -1221,7 +1221,7 @@ class Platform(ChangeLoggedModel): ) -@extras_features(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs']) +@extras_functionality(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs']) class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 774e183ec..9830cdd51 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -14,7 +14,7 @@ from extras.constants import * from extras.models import ( ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) -from extras.utils import FeatureQuerySet +from extras.utils import FunctionalityQueryset from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup from users.api.nested_serializers import NestedUserSerializer @@ -33,7 +33,7 @@ from .nested_serializers import * class GraphSerializer(ValidatedModelSerializer): type = ContentTypeField( - queryset=ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()), + queryset=ContentType.objects.filter(FunctionalityQueryset('graphs').get_queryset()), ) class Meta: @@ -69,7 +69,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): content_type = ContentTypeField( - queryset=ContentType.objects.filter(FeatureQuerySet('export_templates').get_queryset()), + queryset=ContentType.objects.filter(Q(FunctionalityQueryset('export_templates').get_queryset())), ) template_language = ChoiceField( choices=TemplateLanguageChoices, diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 21809c35b..47c05667c 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -22,7 +22,7 @@ from utilities.utils import deepmerge, render_jinja2 from .choices import * from .constants import * from .querysets import ConfigContextQuerySet -from .utils import FeatureQuerySet +from .utils import FunctionalityQueryset __all__ = ( @@ -59,7 +59,7 @@ class Webhook(models.Model): to=ContentType, related_name='webhooks', verbose_name='Object types', - limit_choices_to=FeatureQuerySet('webhooks'), + limit_choices_to=FunctionalityQueryset('webhooks'), help_text="The object(s) to which this Webhook applies." ) name = models.CharField( @@ -224,7 +224,7 @@ class CustomField(models.Model): to=ContentType, related_name='custom_fields', verbose_name='Object(s)', - limit_choices_to=FeatureQuerySet('custom_fields'), + limit_choices_to=FunctionalityQueryset('custom_fields'), help_text='The object(s) to which this field applies.' ) type = models.CharField( @@ -471,7 +471,7 @@ class CustomLink(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FeatureQuerySet('custom_links') + limit_choices_to=FunctionalityQueryset('custom_links') ) name = models.CharField( max_length=100, @@ -519,7 +519,7 @@ class Graph(models.Model): type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FeatureQuerySet('graphs') + limit_choices_to=FunctionalityQueryset('graphs') ) weight = models.PositiveSmallIntegerField( default=1000 @@ -582,7 +582,7 @@ class ExportTemplate(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FeatureQuerySet('export_templates') + limit_choices_to=FunctionalityQueryset('export_templates') ) name = models.CharField( max_length=100 diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index b52091f62..6871b2654 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -8,6 +8,7 @@ from rest_framework import status from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackRole, Region, Site from extras.api.views import ScriptViewSet from extras.choices import * +from extras.constants import GRAPH_MODELS from extras.models import ConfigContext, Graph, ExportTemplate, Tag from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from tenancy.models import Tenant, TenantGroup @@ -34,7 +35,7 @@ class AppTest(APITestCase): self.assertEqual(choices_to_dict(response.data.get('export-template:template_language')), TemplateLanguageChoices.as_dict()) # Graph - content_types = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()) + content_types = ContentType.objects.filter(GRAPH_MODELS) graph_type_choices = { "{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types } diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index 9b02293eb..126414cfd 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -3,6 +3,7 @@ from django.test import TestCase from dcim.models import DeviceRole, Platform, Region, Site from extras.choices import * +from extras.constants import GRAPH_MODELS from extras.filters import * from extras.models import ConfigContext, ExportTemplate, Graph from tenancy.models import Tenant, TenantGroup @@ -17,7 +18,7 @@ class GraphTestCase(TestCase): def setUpTestData(cls): # Get the first three available types - content_types = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset())[:3] + content_types = ContentType.objects.filter(GRAPH_MODELS)[:3] graphs = ( Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'), @@ -31,7 +32,7 @@ class GraphTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_type(self): - content_type = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()).first() + content_type = ContentType.objects.filter(GRAPH_MODELS).first() params = {'type': content_type.pk} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index 31a312333..d6e55e6f6 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -22,61 +22,55 @@ def is_taggable(obj): class Registry: """ - The registry is a place to hook into for data storage across components + Singleton object used to store important data """ + instance = None - def add_store(self, store_name, initial_value=None): - """ - Given the name of some new data parameter and an optional initial value, setup the registry store - """ - if not hasattr(Registry, store_name): - setattr(Registry, store_name, initial_value) - -registry = Registry() + def __new__(cls): + if cls.instance is not None: + return cls.instance + else: + cls.instance = super().__new__(cls) + cls.model_functionality_store = {f: collections.defaultdict(list) for f in EXTRAS_FUNCTIONALITIES} + return cls.instance -# -# Dynamic feature registration -# - -class FeatureQuerySet: +class FunctionalityQueryset: """ Helper class that delays evaluation of the registry contents for the functionaility store until it has been populated. """ - def __init__(self, feature): - self.feature = feature + def __init__(self, functionality): + self.functionality = functionality def __call__(self): return self.get_queryset() def get_queryset(self): """ - Given an extras feature, return a Q object for content type lookup + Given an extras functionality, return a Q object for content type lookup """ query = Q() - #registry = Registry() - for app_label, models in registry.model_feature_store[self.feature].items(): + registry = Registry() + for app_label, models in registry.model_functionality_store[self.functionality].items(): query |= Q(app_label=app_label, model__in=models) return query -registry.add_store('model_feature_store', {f: collections.defaultdict(list) for f in EXTRAS_FUNCTIONALITIES}) - - -def extras_features(features): +def extras_functionality(functionalities): """ - Decorator used to register extras provided features to a model + Decorator used to register extras provided functionalities to a model """ def wrapper(model_class): - if isinstance(features, list) and features: - #registry = Registry() - model_class._extras_feature = [] - for feature in features: - if feature in EXTRAS_FUNCTIONALITIES: + if isinstance(functionalities, list) and functionalities: + registry = Registry() + model_class._extras_functionality = [] + for functionality in functionalities: + if functionality in EXTRAS_FUNCTIONALITIES: + model_class._extras_functionality.append(functionality) app_label, model_name = model_class._meta.label_lower.split('.') - registry.model_feature_store[feature][app_label].append(model_name) + registry.model_functionality_store[functionality][app_label].append(model_name) return model_class return wrapper From 2dc31c0edd51f130051c9ac95dfe142fee57ecf7 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 15 Mar 2020 00:25:46 -0400 Subject: [PATCH 050/124] Revert "implemented registry for extras model functionality" This reverts commit 235d99021bbc329888df924fdec568166c4a264b. --- netbox/dcim/models/__init__.py | 2 - netbox/extras/api/serializers.py | 6 +- netbox/extras/constants.py | 342 +++++++++++++++---------------- netbox/extras/models.py | 11 +- netbox/extras/utils.py | 61 ------ 5 files changed, 173 insertions(+), 249 deletions(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index f702f8dff..1dbfdb76b 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -21,7 +21,6 @@ from dcim.constants import * from dcim.fields import ASNField from dcim.elevations import RackElevationSVG from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem -from extras.utils import extras_functionality from utilities.fields import ColorField, NaturalOrderingField from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object, to_meters @@ -1221,7 +1220,6 @@ class Platform(ChangeLoggedModel): ) -@extras_functionality(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs']) class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 9830cdd51..40606ed8e 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,6 +1,5 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Q from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers @@ -14,7 +13,6 @@ from extras.constants import * from extras.models import ( ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) -from extras.utils import FunctionalityQueryset from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup from users.api.nested_serializers import NestedUserSerializer @@ -33,7 +31,7 @@ from .nested_serializers import * class GraphSerializer(ValidatedModelSerializer): type = ContentTypeField( - queryset=ContentType.objects.filter(FunctionalityQueryset('graphs').get_queryset()), + queryset=ContentType.objects.filter(GRAPH_MODELS), ) class Meta: @@ -69,7 +67,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): content_type = ContentTypeField( - queryset=ContentType.objects.filter(Q(FunctionalityQueryset('export_templates').get_queryset())), + queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS), ) template_language = ChoiceField( choices=TemplateLanguageChoices, diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 5b893b5ad..7bb026d34 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -2,127 +2,127 @@ from django.db.models import Q # Models which support custom fields -#CUSTOMFIELD_MODELS = Q( -# Q(app_label='circuits', model__in=[ -# 'circuit', -# 'provider', -# ]) | -# Q(app_label='dcim', model__in=[ -# 'device', -# 'devicetype', -# 'powerfeed', -# 'rack', -# 'site', -# ]) | -# Q(app_label='ipam', model__in=[ -# 'aggregate', -# 'ipaddress', -# 'prefix', -# 'service', -# 'vlan', -# 'vrf', -# ]) | -# Q(app_label='secrets', model__in=[ -# 'secret', -# ]) | -# Q(app_label='tenancy', model__in=[ -# 'tenant', -# ]) | -# Q(app_label='virtualization', model__in=[ -# 'cluster', -# 'virtualmachine', -# ]) -#) -# -## Custom links -#CUSTOMLINK_MODELS = Q( -# Q(app_label='circuits', model__in=[ -# 'circuit', -# 'provider', -# ]) | -# Q(app_label='dcim', model__in=[ -# 'cable', -# 'device', -# 'devicetype', -# 'powerpanel', -# 'powerfeed', -# 'rack', -# 'site', -# ]) | -# Q(app_label='ipam', model__in=[ -# 'aggregate', -# 'ipaddress', -# 'prefix', -# 'service', -# 'vlan', -# 'vrf', -# ]) | -# Q(app_label='secrets', model__in=[ -# 'secret', -# ]) | -# Q(app_label='tenancy', model__in=[ -# 'tenant', -# ]) | -# Q(app_label='virtualization', model__in=[ -# 'cluster', -# 'virtualmachine', -# ]) -#) -# -## Models which can have Graphs associated with them -#GRAPH_MODELS = Q( -# Q(app_label='circuits', model__in=[ -# 'provider', -# ]) | -# Q(app_label='dcim', model__in=[ -# 'device', -# 'interface', -# 'site', -# ]) -#) -# -## Models which support export templates -#EXPORTTEMPLATE_MODELS = Q( -# Q(app_label='circuits', model__in=[ -# 'circuit', -# 'provider', -# ]) | -# Q(app_label='dcim', model__in=[ -# 'cable', -# 'consoleport', -# 'device', -# 'devicetype', -# 'interface', -# 'inventoryitem', -# 'manufacturer', -# 'powerpanel', -# 'powerport', -# 'powerfeed', -# 'rack', -# 'rackgroup', -# 'region', -# 'site', -# 'virtualchassis', -# ]) | -# Q(app_label='ipam', model__in=[ -# 'aggregate', -# 'ipaddress', -# 'prefix', -# 'service', -# 'vlan', -# 'vrf', -# ]) | -# Q(app_label='secrets', model__in=[ -# 'secret', -# ]) | -# Q(app_label='tenancy', model__in=[ -# 'tenant', -# ]) | -# Q(app_label='virtualization', model__in=[ -# 'cluster', -# 'virtualmachine', -# ]) -#) +CUSTOMFIELD_MODELS = Q( + Q(app_label='circuits', model__in=[ + 'circuit', + 'provider', + ]) | + Q(app_label='dcim', model__in=[ + 'device', + 'devicetype', + 'powerfeed', + 'rack', + 'site', + ]) | + Q(app_label='ipam', model__in=[ + 'aggregate', + 'ipaddress', + 'prefix', + 'service', + 'vlan', + 'vrf', + ]) | + Q(app_label='secrets', model__in=[ + 'secret', + ]) | + Q(app_label='tenancy', model__in=[ + 'tenant', + ]) | + Q(app_label='virtualization', model__in=[ + 'cluster', + 'virtualmachine', + ]) +) + +# Custom links +CUSTOMLINK_MODELS = Q( + Q(app_label='circuits', model__in=[ + 'circuit', + 'provider', + ]) | + Q(app_label='dcim', model__in=[ + 'cable', + 'device', + 'devicetype', + 'powerpanel', + 'powerfeed', + 'rack', + 'site', + ]) | + Q(app_label='ipam', model__in=[ + 'aggregate', + 'ipaddress', + 'prefix', + 'service', + 'vlan', + 'vrf', + ]) | + Q(app_label='secrets', model__in=[ + 'secret', + ]) | + Q(app_label='tenancy', model__in=[ + 'tenant', + ]) | + Q(app_label='virtualization', model__in=[ + 'cluster', + 'virtualmachine', + ]) +) + +# Models which can have Graphs associated with them +GRAPH_MODELS = Q( + Q(app_label='circuits', model__in=[ + 'provider', + ]) | + Q(app_label='dcim', model__in=[ + 'device', + 'interface', + 'site', + ]) +) + +# Models which support export templates +EXPORTTEMPLATE_MODELS = Q( + Q(app_label='circuits', model__in=[ + 'circuit', + 'provider', + ]) | + Q(app_label='dcim', model__in=[ + 'cable', + 'consoleport', + 'device', + 'devicetype', + 'interface', + 'inventoryitem', + 'manufacturer', + 'powerpanel', + 'powerport', + 'powerfeed', + 'rack', + 'rackgroup', + 'region', + 'site', + 'virtualchassis', + ]) | + Q(app_label='ipam', model__in=[ + 'aggregate', + 'ipaddress', + 'prefix', + 'service', + 'vlan', + 'vrf', + ]) | + Q(app_label='secrets', model__in=[ + 'secret', + ]) | + Q(app_label='tenancy', model__in=[ + 'tenant', + ]) | + Q(app_label='virtualization', model__in=[ + 'cluster', + 'virtualmachine', + ]) +) # Report logging levels LOG_DEFAULT = 0 @@ -141,58 +141,48 @@ LOG_LEVEL_CODES = { HTTP_CONTENT_TYPE_JSON = 'application/json' # Models which support registered webhooks -#WEBHOOK_MODELS = Q( -# Q(app_label='circuits', model__in=[ -# 'circuit', -# 'provider', -# ]) | -# Q(app_label='dcim', model__in=[ -# 'cable', -# 'consoleport', -# 'consoleserverport', -# 'device', -# 'devicebay', -# 'devicetype', -# 'frontport', -# 'interface', -# 'inventoryitem', -# 'manufacturer', -# 'poweroutlet', -# 'powerpanel', -# 'powerport', -# 'powerfeed', -# 'rack', -# 'rearport', -# 'region', -# 'site', -# 'virtualchassis', -# ]) | -# Q(app_label='ipam', model__in=[ -# 'aggregate', -# 'ipaddress', -# 'prefix', -# 'service', -# 'vlan', -# 'vrf', -# ]) | -# Q(app_label='secrets', model__in=[ -# 'secret', -# ]) | -# Q(app_label='tenancy', model__in=[ -# 'tenant', -# ]) | -# Q(app_label='virtualization', model__in=[ -# 'cluster', -# 'virtualmachine', -# ]) -#) - - -# Registerable extras functionalities -EXTRAS_FUNCTIONALITIES = [ - 'custom_fields', - 'custom_links', - 'graphs', - 'export_templates', - 'webhooks' -] +WEBHOOK_MODELS = Q( + Q(app_label='circuits', model__in=[ + 'circuit', + 'provider', + ]) | + Q(app_label='dcim', model__in=[ + 'cable', + 'consoleport', + 'consoleserverport', + 'device', + 'devicebay', + 'devicetype', + 'frontport', + 'interface', + 'inventoryitem', + 'manufacturer', + 'poweroutlet', + 'powerpanel', + 'powerport', + 'powerfeed', + 'rack', + 'rearport', + 'region', + 'site', + 'virtualchassis', + ]) | + Q(app_label='ipam', model__in=[ + 'aggregate', + 'ipaddress', + 'prefix', + 'service', + 'vlan', + 'vrf', + ]) | + Q(app_label='secrets', model__in=[ + 'secret', + ]) | + Q(app_label='tenancy', model__in=[ + 'tenant', + ]) | + Q(app_label='virtualization', model__in=[ + 'cluster', + 'virtualmachine', + ]) +) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 47c05667c..d81fbeab9 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -22,7 +22,6 @@ from utilities.utils import deepmerge, render_jinja2 from .choices import * from .constants import * from .querysets import ConfigContextQuerySet -from .utils import FunctionalityQueryset __all__ = ( @@ -59,7 +58,7 @@ class Webhook(models.Model): to=ContentType, related_name='webhooks', verbose_name='Object types', - limit_choices_to=FunctionalityQueryset('webhooks'), + limit_choices_to=WEBHOOK_MODELS, help_text="The object(s) to which this Webhook applies." ) name = models.CharField( @@ -224,7 +223,7 @@ class CustomField(models.Model): to=ContentType, related_name='custom_fields', verbose_name='Object(s)', - limit_choices_to=FunctionalityQueryset('custom_fields'), + limit_choices_to=CUSTOMFIELD_MODELS, help_text='The object(s) to which this field applies.' ) type = models.CharField( @@ -471,7 +470,7 @@ class CustomLink(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FunctionalityQueryset('custom_links') + limit_choices_to=CUSTOMLINK_MODELS ) name = models.CharField( max_length=100, @@ -519,7 +518,7 @@ class Graph(models.Model): type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FunctionalityQueryset('graphs') + limit_choices_to=GRAPH_MODELS ) weight = models.PositiveSmallIntegerField( default=1000 @@ -582,7 +581,7 @@ class ExportTemplate(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FunctionalityQueryset('export_templates') + limit_choices_to=EXPORTTEMPLATE_MODELS ) name = models.CharField( max_length=100 diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index d6e55e6f6..ca3a72526 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -1,11 +1,6 @@ -import collections - -from django.db.models import Q from taggit.managers import _TaggableManager from utilities.querysets import DummyQuerySet -from extras.constants import EXTRAS_FUNCTIONALITIES - def is_taggable(obj): """ @@ -18,59 +13,3 @@ def is_taggable(obj): if isinstance(obj.tags, DummyQuerySet): return True return False - - -class Registry: - """ - Singleton object used to store important data - """ - instance = None - - def __new__(cls): - if cls.instance is not None: - return cls.instance - else: - cls.instance = super().__new__(cls) - cls.model_functionality_store = {f: collections.defaultdict(list) for f in EXTRAS_FUNCTIONALITIES} - return cls.instance - - -class FunctionalityQueryset: - """ - Helper class that delays evaluation of the registry contents for the functionaility store - until it has been populated. - """ - - def __init__(self, functionality): - self.functionality = functionality - - def __call__(self): - return self.get_queryset() - - def get_queryset(self): - """ - Given an extras functionality, return a Q object for content type lookup - """ - query = Q() - registry = Registry() - for app_label, models in registry.model_functionality_store[self.functionality].items(): - query |= Q(app_label=app_label, model__in=models) - - return query - - -def extras_functionality(functionalities): - """ - Decorator used to register extras provided functionalities to a model - """ - def wrapper(model_class): - if isinstance(functionalities, list) and functionalities: - registry = Registry() - model_class._extras_functionality = [] - for functionality in functionalities: - if functionality in EXTRAS_FUNCTIONALITIES: - model_class._extras_functionality.append(functionality) - app_label, model_name = model_class._meta.label_lower.split('.') - registry.model_functionality_store[functionality][app_label].append(model_name) - return model_class - return wrapper From 0574ac7530209d07f1ab0775b28763cb958cd25e Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 15 Mar 2020 00:48:05 -0400 Subject: [PATCH 051/124] fixed migration order --- ...andardize_description.py => 0040_standardize_description.py} | 2 +- .../{0040_tag_description.py => 0041_tag_description.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename netbox/extras/migrations/{0039_standardize_description.py => 0040_standardize_description.py} (90%) rename netbox/extras/migrations/{0040_tag_description.py => 0041_tag_description.py} (90%) diff --git a/netbox/extras/migrations/0039_standardize_description.py b/netbox/extras/migrations/0040_standardize_description.py similarity index 90% rename from netbox/extras/migrations/0039_standardize_description.py rename to netbox/extras/migrations/0040_standardize_description.py index e56f3e1eb..fdc5da2da 100644 --- a/netbox/extras/migrations/0039_standardize_description.py +++ b/netbox/extras/migrations/0040_standardize_description.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('extras', '0038_webhook_template_support'), + ('extras', '0039_update_features_content_types'), ] operations = [ diff --git a/netbox/extras/migrations/0040_tag_description.py b/netbox/extras/migrations/0041_tag_description.py similarity index 90% rename from netbox/extras/migrations/0040_tag_description.py rename to netbox/extras/migrations/0041_tag_description.py index 9d17b205f..63ac5c49a 100644 --- a/netbox/extras/migrations/0040_tag_description.py +++ b/netbox/extras/migrations/0041_tag_description.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('extras', '0039_standardize_description'), + ('extras', '0040_standardize_description'), ] operations = [ From 8364694fb4bf606bf920cc81e7592b868576d10e Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 15 Mar 2020 23:45:18 -0400 Subject: [PATCH 052/124] added plugin template content injection to primary model detail views --- netbox/extras/plugins/__init__.py | 87 +++++++++++++++++++ netbox/extras/plugins/signals.py | 4 +- .../extras/plugins/templatetags/__init__.py | 0 netbox/extras/templatetags/plugins.py | 55 +++++++++--- netbox/templates/circuits/circuit.html | 9 ++ netbox/templates/circuits/provider.html | 9 ++ netbox/templates/dcim/cable.html | 9 ++ netbox/templates/dcim/device.html | 7 ++ netbox/templates/dcim/devicetype.html | 9 ++ netbox/templates/dcim/powerfeed.html | 9 ++ netbox/templates/dcim/powerpanel.html | 9 ++ netbox/templates/dcim/rack.html | 9 ++ netbox/templates/dcim/rackreservation.html | 9 ++ netbox/templates/dcim/site.html | 8 ++ netbox/templates/ipam/aggregate.html | 9 ++ netbox/templates/ipam/ipaddress.html | 9 ++ netbox/templates/ipam/prefix.html | 9 ++ netbox/templates/ipam/service.html | 13 ++- netbox/templates/ipam/vlan.html | 9 ++ netbox/templates/ipam/vrf.html | 9 ++ netbox/templates/secrets/secret.html | 9 ++ netbox/templates/tenancy/tenant.html | 9 ++ netbox/templates/virtualization/cluster.html | 9 ++ .../virtualization/virtualmachine.html | 9 ++ 24 files changed, 313 insertions(+), 14 deletions(-) delete mode 100644 netbox/extras/plugins/templatetags/__init__.py diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index e69de29bb..46ca58336 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -0,0 +1,87 @@ +import collections +import inspect + +from django.core.exceptions import ImproperlyConfigured +from django.template.loader import get_template + +from extras.utils import registry +from .signals import register_detail_page_content_classes + + +class PluginTemplateContent: + """ + This class is used to register plugin content to be injected into core NetBox templates. + It contains methods that are overriden by plugin authors to return template content. + + The `model` attribute on the class defines the which model detail page this class renders + content for. It should be set as a string in the form '.'. + """ + model = None + + def __init__(self, obj): + self.obj = obj + + def render(self, template, extra_context=None): + """ + Convenience menthod for rendering the provided template name. The detail page object is automatically + passed into the template context as `obj` but an additional context dictionary may be passed as `extra_context`. + """ + context = {'obj': self.obj} + if isinstance(extra_context, dict): + context.update(extra_context) + + return get_template(template).render(context) + + def left_page(self): + """ + Content that will be rendered on the left of the detail page view. Content should be returned as an + HTML string. Note that content does not need to be marked as safe because this is automatically handled. + """ + raise NotImplementedError + + def right_page(self): + """ + Content that will be rendered on the right of the detail page view. Content should be returned as an + HTML string. Note that content does not need to be marked as safe because this is automatically handled. + """ + raise NotImplementedError + + def full_width_page(self): + """ + Content that will be rendered within the full width of the detail page view. Content should be returned as an + HTML string. Note that content does not need to be marked as safe because this is automatically handled. + """ + raise NotImplementedError + + def buttons(self): + """ + Buttons that will be rendered and added to the existing list of buttons on the detail page view. Content + should be returned as an HTML string. Note that content does not need to be marked as safe because this is + automatically handled. + """ + raise NotImplementedError + + +def register_content_classes(): + registry.plugin_template_content_classes = collections.defaultdict(list) + + responses = register_detail_page_content_classes.send('registration_event') + for receiver, response in responses: + if not isinstance(response, list): + response = [response] + for template_class in response: + if not inspect.isclass(template_class): + raise TypeError('Plugin content class {} was passes as an instance!'.format(template_class)) + if not issubclass(template_class, PluginTemplateContent): + raise TypeError('{} is not a subclass of extras.plugins.PluginTemplateContent!'.format(template_class)) + if template_class.model is None: + raise TypeError('Plugin content class {} does not define a valid model!'.format(template_class)) + + registry.plugin_template_content_classes[template_class.model].append(template_class) + + +def get_content_classes(model): + if not hasattr(registry, 'plugin_template_content_classes'): + register_content_classes() + + return registry.plugin_template_content_classes.get(model, []) diff --git a/netbox/extras/plugins/signals.py b/netbox/extras/plugins/signals.py index 7d0567b1b..0e5576b67 100644 --- a/netbox/extras/plugins/signals.py +++ b/netbox/extras/plugins/signals.py @@ -27,8 +27,8 @@ class PluginSignal(django.dispatch.Signal): """ -This signal collects templates which render buttons for object detail pages +This signal collects templates which render content for object detail pages """ -register_detail_page_buttons = PluginSignal( +register_detail_page_content_classes = PluginSignal( providing_args=[] ) diff --git a/netbox/extras/plugins/templatetags/__init__.py b/netbox/extras/plugins/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/netbox/extras/templatetags/plugins.py b/netbox/extras/templatetags/plugins.py index dc3443d0c..c4a3002e7 100644 --- a/netbox/extras/templatetags/plugins.py +++ b/netbox/extras/templatetags/plugins.py @@ -2,26 +2,59 @@ from django import template as template_ from django.template.loader import get_template from django.utils.safestring import mark_safe -from extras.plugins.signals import register_detail_page_buttons +from extras.plugins import get_content_classes register = template_.Library() +def _get_registered_content(obj, method): + """ + Given an object and a PluginTemplateContent method name, return all the registered content for the + object's model. + """ + html = '' + + plugin_template_classes = get_content_classes(obj._meta.label_lower) + for plugin_template_class in plugin_template_classes: + plugin_template_renderer = plugin_template_class(obj) + try: + content = getattr(plugin_template_renderer, method)() + except NotImplementedError: + # This content renderer class does not define content for this method + continue + html += content + + return mark_safe(html) + + @register.simple_tag() def plugin_buttons(obj): """ Fire signal to collect all buttons registered by plugins """ - html = '' - responses = register_detail_page_buttons.send(obj) - for receiver, response in responses: - if not isinstance(response, list): - response = [response] - for template in response: - if isinstance(template, str): - template_text = get_template(template).render({'obj': obj}) - html += template_text + return _get_registered_content(obj, 'buttons') - return mark_safe(html) +@register.simple_tag() +def plugin_left_page(obj): + """ + Fire signal to collect all left page content registered by plugins + """ + return _get_registered_content(obj, 'left_page') + + +@register.simple_tag() +def plugin_right_page(obj): + """ + Fire signal to collect all right page content registered by plugins + """ + return _get_registered_content(obj, 'right_page') + + +@register.simple_tag() +def plugin_full_width_page(obj): + """ + Fire signal to collect all full width page content registered by plugins + """ + return _get_registered_content(obj, 'full_width_page') diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 4d7fe9fe2..6b380dc38 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block title %}{{ circuit }}{% endblock %} @@ -28,6 +29,7 @@
+ {% plugin_buttons circuit %} {% if perms.circuits.add_circuit %} {% clone_button circuit %} {% endif %} @@ -125,10 +127,17 @@ {% endif %}
+ {% plugin_left_page circuit %}
{% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %} {% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %} + {% plugin_right_page circuit %} +
+ +
+
+ {% plugin_full_width_page circuit %}
{% endblock %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index faeb516ee..c8b6cd66f 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -3,6 +3,7 @@ {% load static %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block title %}{{ provider }}{% endblock %} @@ -28,6 +29,7 @@
+ {% plugin_buttons provider %} {% if show_graphs %}
+ {% plugin_left_page provider %}
@@ -132,9 +135,15 @@ {% endif %}
{% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} + {% plugin_right_page provider %}
{% include 'inc/modal.html' with name='graphs' title='Graphs' %} +
+
+ {% plugin_full_width_page provider %} +
+
{% endblock %} {% block javascript %} diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index a78879b23..a74debfe9 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -13,6 +14,7 @@
+ {% plugin_buttons cable %} {% if perms.dcim.change_cable %} {% edit_button cable %} {% endif %} @@ -79,6 +81,7 @@
+ {% plugin_left_page cable %}
@@ -93,6 +96,12 @@
{% include 'dcim/inc/cable_termination.html' with termination=cable.termination_b %}
+ {% plugin_right_page cable %} + + +
+
+ {% plugin_full_width_page cable %}
{% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 523e3383d..34eee3f55 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -333,6 +333,7 @@ {% endif %} + {% plugin_left_page device %}
{% if console_ports or power_ports %} @@ -499,6 +500,12 @@
None found
{% endif %}
+ {% plugin_right_page device %} + + +
+
+ {% plugin_full_width_page device %}
diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 352141a9a..4d401da88 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %} @@ -16,6 +17,7 @@
+ {% plugin_buttons devicetype %} {% if perms.dcim.change_devicetype %}
+ {% plugin_left_page devicetype %}
{% include 'inc/custom_fields_panel.html' with obj=devicetype %} @@ -155,6 +158,7 @@ {% endif %}
+ {% plugin_right_page devicetype %} {% if devicetype.consoleport_templates.exists or devicetype.powerport_templates.exists %} @@ -167,6 +171,11 @@ {% endif %} +
+
+ {% plugin_full_width_page devicetype %} +
+
{% if devicetype.is_parent_device or devicebay_table.rows %}
diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index ca717b5e1..026d97313 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -3,6 +3,7 @@ {% load static %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -31,6 +32,7 @@
+ {% plugin_buttons powerfeed %} {% if perms.dcim.add_powerfeed %} {% clone_button powerfeed %} {% endif %} @@ -123,6 +125,7 @@
{% include 'inc/custom_fields_panel.html' with obj=powerfeed %} {% include 'extras/inc/tags_panel.html' with tags=powerfeed.tags.all url='dcim:powerfeed_list' %} + {% plugin_left_page powerfeed %}
@@ -164,6 +167,12 @@ {% endif %}
+ {% plugin_right_page powerfeed %} + + +
+
+ {% plugin_full_width_page powerfeed %}
{% endblock %} diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index 6d47e08b1..a2d3376d7 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -3,6 +3,7 @@ {% load custom_links %} {% load helpers %} {% load static %} +{% load plugins %} {% block header %}
@@ -30,6 +31,7 @@
+ {% plugin_buttons powerpanel %} {% if perms.dcim.change_powerpanel %} {% edit_button powerpanel %} {% endif %} @@ -80,9 +82,16 @@
+ {% plugin_left_page powerpanel %}
{% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %} + {% plugin_right_page powerpanel %} +
+ +
+
+ {% plugin_full_width_page powerpanel %}
{% endblock %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index ecd17172b..756f9619e 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -3,6 +3,7 @@ {% load custom_links %} {% load helpers %} {% load static %} +{% load plugins %} {% block header %}
@@ -27,6 +28,7 @@
{% endif %} + {% plugin_left_page rack %}
@@ -369,6 +372,12 @@
{% endif %}
+ {% plugin_right_page rack %} + + +
+
+ {% plugin_full_width_page rack %}
{% endblock %} diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html index ef9e49d23..81592904f 100644 --- a/netbox/templates/dcim/rackreservation.html +++ b/netbox/templates/dcim/rackreservation.html @@ -3,6 +3,7 @@ {% load custom_links %} {% load helpers %} {% load static %} +{% load plugins %} {% block header %}
@@ -27,6 +28,7 @@
+ {% plugin_buttons rackreservation %} {% if perms.dcim.change_rackreservation %} {% edit_button rackreservation %} {% endif %} @@ -119,6 +121,7 @@
+ {% plugin_left_page rackreservation %}
{% with rack=rackreservation.rack %} @@ -137,6 +140,12 @@
{% endwith %} + {% plugin_right_page rackreservation %} + + +
+
+ {% plugin_full_width_page rackreservation %}
{% endblock %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 6a0b836f8..16cee782c 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -5,6 +5,7 @@ {% load plugins %} {% load static %} {% load tz %} +{% load plugins %} {% block header %}
@@ -214,6 +215,7 @@ {% endif %}
+ {% plugin_left_page site %}
@@ -288,9 +290,15 @@
{% endif %}
+ {% plugin_right_page site %} {% include 'inc/modal.html' with name='graphs' title='Graphs' %} +
+
+ {% plugin_full_width_page site %} +
+
{% endblock %} {% block javascript %} diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 43cfb10a0..9810e3689 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -26,6 +27,7 @@
+ {% plugin_buttons aggregate %} {% if perms.ipam.add_aggregate %} {% clone_button aggregate %} {% endif %} @@ -88,10 +90,17 @@
+ {% plugin_left_page aggregate %}
{% include 'inc/custom_fields_panel.html' with obj=aggregate %} {% include 'extras/inc/tags_panel.html' with tags=aggregate.tags.all url='ipam:aggregate_list' %} + {% plugin_right_page aggregate %} +
+ +
+
+ {% plugin_full_width_page aggregate %}
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 83c34cd6b..a627b8d69 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -28,6 +29,7 @@
+ {% plugin_buttons ipaddress %} {% if perms.ipam.add_ipaddress %} {% clone_button ipaddress %} {% endif %} @@ -152,6 +154,7 @@
{% include 'inc/custom_fields_panel.html' with obj=ipaddress %} {% include 'extras/inc/tags_panel.html' with tags=ipaddress.tags.all url='ipam:ipaddress_list' %} + {% plugin_left_page ipaddress %}
{% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %} @@ -159,6 +162,12 @@ {% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %} {% endif %} {% include 'utilities/obj_table.html' with table=related_ips_table table_template='panel_table.html' heading='Related IP Addresses' panel_class='default noprint' %} + {% plugin_right_page ipaddress %}
+
+
+ {% plugin_full_width_page ipaddress %} +
+
{% endblock %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 5d5490937..d6d7ef4d8 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -28,6 +29,7 @@
+ {% plugin_buttons prefix %} {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %} Add Child Prefix @@ -187,12 +189,19 @@
{% include 'inc/custom_fields_panel.html' with obj=prefix %} {% include 'extras/inc/tags_panel.html' with tags=prefix.tags.all url='ipam:prefix_list' %} + {% plugin_left_page prefix %}
{% if duplicate_prefix_table.rows %} {% include 'panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' panel_class='danger' %} {% endif %} {% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' panel_class='default' %} + {% plugin_right_page prefix %} +
+ +
+
+ {% plugin_full_width_page prefix %}
{% endblock %} diff --git a/netbox/templates/ipam/service.html b/netbox/templates/ipam/service.html index b845aca17..2d4e69fa5 100644 --- a/netbox/templates/ipam/service.html +++ b/netbox/templates/ipam/service.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block content %}
@@ -26,6 +27,7 @@
+ {% plugin_buttons service %} {% if perms.dcim.change_service %} {% edit_button service %} {% endif %} @@ -81,6 +83,15 @@
{% include 'inc/custom_fields_panel.html' with obj=service %} {% include 'extras/inc/tags_panel.html' with tags=service.tags.all url='ipam:service_list' %} - + {% plugin_left_page service %} + +
+ {% plugin_right_page service %} +
+ +
+
+ {% plugin_full_width_page service %} +
{% endblock %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 246f3c866..6b867762b 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -31,6 +32,7 @@
+ {% plugin_buttons vlan %} {% if perms.ipam.add_vlan %} {% clone_button vlan %} {% endif %} @@ -139,6 +141,7 @@
{% include 'inc/custom_fields_panel.html' with obj=vlan %} {% include 'extras/inc/tags_panel.html' with tags=vlan.tags.all url='ipam:vlan_list' %} + {% plugin_left_page vlan %}
@@ -155,6 +158,12 @@
{% endif %}
+ {% plugin_right_page vlan %} + + +
+
+ {% plugin_full_width_page vlan %}
{% endblock %} diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 7bb2dea25..e448743f1 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -25,6 +26,7 @@
+ {% plugin_buttons vrf %} {% if perms.ipam.add_vrf %} {% clone_button vrf %} {% endif %} @@ -97,9 +99,16 @@
{% include 'extras/inc/tags_panel.html' with tags=vrf.tags.all url='ipam:vrf_list' %} + {% plugin_left_page vrf %}
{% include 'inc/custom_fields_panel.html' with obj=vrf %} + {% plugin_right_page vrf %} +
+ +
+
+ {% plugin_full_width_page vrf %}
{% endblock %} diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index 6045897c9..6de32b72c 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -4,6 +4,7 @@ {% load helpers %} {% load secret_helpers %} {% load static %} +{% load plugins %} {% block header %}
@@ -16,6 +17,7 @@
+ {% plugin_buttons secret %} {% if perms.secrets.change_secret %} {% edit_button secret %} {% endif %} @@ -65,6 +67,7 @@
{% include 'inc/custom_fields_panel.html' with obj=secret %} + {% plugin_left_page secret %}
{% if secret|decryptable_by:request.user %} @@ -100,6 +103,12 @@
{% endif %} {% include 'extras/inc/tags_panel.html' with tags=secret.tags.all url='secrets:secret_list' %} + {% plugin_right_page secret %} + + +
+
+ {% plugin_full_width_page secret %}
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 4ef26c451..5232d1a86 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -28,6 +29,7 @@
+ {% plugin_buttons tenant %} {% if perms.tenancy.add_tenant %} {% clone_button tenant %} {% endif %} @@ -93,6 +95,7 @@ {% endif %}
+ {% plugin_left_page tenant %}
@@ -146,6 +149,12 @@
+ {% plugin_right_page tenant %} + + +
+
+ {% plugin_full_width_page tenant %}
{% endblock %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 4070977bc..5a9d26837 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -2,6 +2,7 @@ {% load buttons %} {% load custom_links %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -28,6 +29,7 @@
+ {% plugin_buttons cluster %} {% if perms.virtualization.add_cluster %} {% clone_button cluster %} {% endif %} @@ -121,6 +123,7 @@ {% endif %}
+ {% plugin_left_page cluster %}
@@ -148,6 +151,12 @@ {% endif %}
+ {% plugin_right_page cluster %}
+
+
+ {% plugin_full_width_page cluster %} +
+
{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 33dd8130a..ba6a4d33e 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -3,6 +3,7 @@ {% load custom_links %} {% load static %} {% load helpers %} +{% load plugins %} {% block header %}
@@ -28,6 +29,7 @@
+ {% plugin_buttons virtualmachine %} {% if perms.virtualization.add_virtualmachine %} {% clone_button virtualmachine %} {% endif %} @@ -158,6 +160,7 @@ {% endif %}
+ {% plugin_left_page virtualmachine %}
@@ -235,6 +238,12 @@
{% endif %}
+ {% plugin_right_page virtualmachine %} + + +
+
+ {% plugin_full_width_page virtualmachine %}
From 457354c244676b7586e82bb8ba8cd58e98b7c2c6 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 17 Mar 2020 00:03:58 -0400 Subject: [PATCH 053/124] inject origional context as obj_context --- netbox/extras/plugins/__init__.py | 11 ++++++--- netbox/extras/templatetags/plugins.py | 32 +++++++++++++-------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index 46ca58336..af94b9373 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -18,15 +18,20 @@ class PluginTemplateContent: """ model = None - def __init__(self, obj): + def __init__(self, obj, context): self.obj = obj + self.context = context def render(self, template, extra_context=None): """ Convenience menthod for rendering the provided template name. The detail page object is automatically - passed into the template context as `obj` but an additional context dictionary may be passed as `extra_context`. + passed into the template context as `obj` and the origional detail page's context is available as + `obj_context`. An additional context dictionary may be passed as `extra_context`. """ - context = {'obj': self.obj} + context = { + 'obj': self.obj, + 'obj_context': self.context + } if isinstance(extra_context, dict): context.update(extra_context) diff --git a/netbox/extras/templatetags/plugins.py b/netbox/extras/templatetags/plugins.py index c4a3002e7..384b08b6a 100644 --- a/netbox/extras/templatetags/plugins.py +++ b/netbox/extras/templatetags/plugins.py @@ -8,16 +8,16 @@ from extras.plugins import get_content_classes register = template_.Library() -def _get_registered_content(obj, method): +def _get_registered_content(obj, method, context): """ - Given an object and a PluginTemplateContent method name, return all the registered content for the - object's model. + Given an object and a PluginTemplateContent method name and the template context, return all the + registered content for the object's model. """ html = '' plugin_template_classes = get_content_classes(obj._meta.label_lower) for plugin_template_class in plugin_template_classes: - plugin_template_renderer = plugin_template_class(obj) + plugin_template_renderer = plugin_template_class(obj, context) try: content = getattr(plugin_template_renderer, method)() except NotImplementedError: @@ -28,33 +28,33 @@ def _get_registered_content(obj, method): return mark_safe(html) -@register.simple_tag() -def plugin_buttons(obj): +@register.simple_tag(takes_context=True) +def plugin_buttons(context, obj): """ Fire signal to collect all buttons registered by plugins """ - return _get_registered_content(obj, 'buttons') + return _get_registered_content(obj, 'buttons', context) -@register.simple_tag() -def plugin_left_page(obj): +@register.simple_tag(takes_context=True) +def plugin_left_page(context, obj): """ Fire signal to collect all left page content registered by plugins """ - return _get_registered_content(obj, 'left_page') + return _get_registered_content(obj, 'left_page', context) -@register.simple_tag() -def plugin_right_page(obj): +@register.simple_tag(takes_context=True) +def plugin_right_page(context, obj): """ Fire signal to collect all right page content registered by plugins """ - return _get_registered_content(obj, 'right_page') + return _get_registered_content(obj, 'right_page', context) -@register.simple_tag() -def plugin_full_width_page(obj): +@register.simple_tag(takes_context=True) +def plugin_full_width_page(context, obj): """ Fire signal to collect all full width page content registered by plugins """ - return _get_registered_content(obj, 'full_width_page') + return _get_registered_content(obj, 'full_width_page', context) From 981c98223707f066dcdac5fb4f43b27e953da4a8 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 17 Mar 2020 02:35:12 -0400 Subject: [PATCH 054/124] added support for plugin nav bar links --- netbox/extras/plugins/__init__.py | 89 ++++++++++++++++++- netbox/extras/plugins/context_processors.py | 12 +++ netbox/extras/plugins/signals.py | 10 ++- netbox/templates/inc/nav_menu.html | 3 + .../templates/inc/plugin_nav_menu_items.html | 29 ++++++ netbox/utilities/choices.py | 26 ++++++ 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 netbox/extras/plugins/context_processors.py create mode 100644 netbox/templates/inc/plugin_nav_menu_items.html diff --git a/netbox/extras/plugins/__init__.py b/netbox/extras/plugins/__init__.py index af94b9373..f3ce7dcad 100644 --- a/netbox/extras/plugins/__init__.py +++ b/netbox/extras/plugins/__init__.py @@ -1,13 +1,18 @@ import collections +import importlib import inspect from django.core.exceptions import ImproperlyConfigured from django.template.loader import get_template from extras.utils import registry -from .signals import register_detail_page_content_classes +from .signals import register_detail_page_content_classes, register_nav_menu_link_classes +# +# Template content injection +# + class PluginTemplateContent: """ This class is used to register plugin content to be injected into core NetBox templates. @@ -68,6 +73,9 @@ class PluginTemplateContent: def register_content_classes(): + """ + Helper method that populates the registry with all template content classes that have been registered by plugins + """ registry.plugin_template_content_classes = collections.defaultdict(list) responses = register_detail_page_content_classes.send('registration_event') @@ -86,7 +94,86 @@ def register_content_classes(): def get_content_classes(model): + """ + Given a model string, return the list of all registered template content classes. + Populate the registry if it is empty. + """ if not hasattr(registry, 'plugin_template_content_classes'): register_content_classes() return registry.plugin_template_content_classes.get(model, []) + + +# +# Nav menu links +# + +class PluginNavMenuLink: + """ + This class represents a nav menu item. This constitutes primary link and its text, but also allows for + specifying additional link buttons that appear to the right of the item in the van menu. + + Links are specified as Django reverse URL strings. + Buttons are each specified as a list of PluginNavMenuButton instances. + """ + link = None + link_text = None + link_permission = None + buttons = [] + + +class PluginNavMenuButton: + """ + This class represents a button which is a part of the nav menu link item. + Note that button colors should come from ButtonColorChoices + """ + def __init__(self, link, title, icon_class, color, permission=None): + self.link = link + self.title = title + self.icon_class = icon_class + self.color = color + self.permission = permission + + +def register_nav_menu_links(): + """ + Helper method that populates the registry with all nav menu link classes that have been registered by plugins + """ + registry.plugin_nav_menu_link_classes = {} + + responses = register_nav_menu_link_classes.send('registration_event') + for receiver, response in responses: + + # Import the app config for the plugin to get the name to be used as the nav menu section text + module = importlib.import_module(receiver.__module__.split('.')[0]) + default_app_config = getattr(module, 'default_app_config') + module, app_config = default_app_config.rsplit('.', 1) + app_config = getattr(importlib.import_module(module), app_config) + section_name = app_config.NetBoxPluginMeta.name + + if not isinstance(response, list): + response = [response] + for link_class in response: + if not inspect.isclass(link_class): + raise TypeError('Plugin nav menu link class {} was passes as an instance!'.format(link_class)) + if not issubclass(link_class, PluginNavMenuLink): + raise TypeError('{} is not a subclass of extras.plugins.PluginNavMenuLink!'.format(link_class)) + if link_class.link is None or link_class.link_text is None: + raise TypeError('Plugin nav menu link {} must specify at least link and link_text'.format(link_class)) + + for button in link_class.buttons: + if not isinstance(button, PluginNavMenuButton): + raise TypeError('{} must be an instance of PluginNavMenuButton!'.format(button)) + + registry.plugin_nav_menu_link_classes[section_name] = response + + +def get_nav_menu_link_classes(): + """ + Return the list of all registered nav menu link classes. + Populate the registry if it is empty. + """ + if not hasattr(registry, 'plugin_nav_menu_link_classes'): + register_nav_menu_links() + + return registry.plugin_nav_menu_link_classes diff --git a/netbox/extras/plugins/context_processors.py b/netbox/extras/plugins/context_processors.py new file mode 100644 index 000000000..14b05b874 --- /dev/null +++ b/netbox/extras/plugins/context_processors.py @@ -0,0 +1,12 @@ +from . import get_nav_menu_link_classes + + +def nav_menu_links(request): + """ + Retrieve and expose all plugin registered nav links + """ + nav_menu_links = get_nav_menu_link_classes() + + return { + 'plugin_nav_menu_links': nav_menu_links + } diff --git a/netbox/extras/plugins/signals.py b/netbox/extras/plugins/signals.py index 0e5576b67..7ebb549db 100644 --- a/netbox/extras/plugins/signals.py +++ b/netbox/extras/plugins/signals.py @@ -27,8 +27,16 @@ class PluginSignal(django.dispatch.Signal): """ -This signal collects templates which render content for object detail pages +This signal collects template content classes which render content for object detail pages """ register_detail_page_content_classes = PluginSignal( providing_args=[] ) + + +""" +This signal collects nav menu link classes +""" +register_nav_menu_link_classes = PluginSignal( + providing_args=[] +) diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index e65d42623..d42bb7737 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -504,6 +504,9 @@ + {% if plugin_nav_menu_links %} + {% include 'inc/plugin_nav_menu_items.html' %} + {% endif %} {% endif %} - {% if plugin_nav_menu_links %} - {% include 'inc/plugin_nav_menu_items.html' %} + {% if plugin_menu_items %} + {% include 'inc/plugin_menu_items.html' %} {% endif %} {% endif %} diff --git a/netbox/templates/inc/plugin_nav_menu_items.html b/netbox/templates/inc/plugin_menu_items.html similarity index 71% rename from netbox/templates/inc/plugin_nav_menu_items.html rename to netbox/templates/inc/plugin_menu_items.html index 55aab84c1..5639d39c7 100644 --- a/netbox/templates/inc/plugin_nav_menu_items.html +++ b/netbox/templates/inc/plugin_menu_items.html @@ -1,24 +1,24 @@