diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 073e8dc5c..3c52c973c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
- placeholder: v2.11.2
+ placeholder: v2.11.3
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 6ea8b6597..9181f7ce4 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v2.10.4
+ placeholder: v2.11.3
validations:
required: true
- type: dropdown
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 45f233a55..0f617e8aa 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -17,8 +17,8 @@ jobs:
necessary.
close-pr-message: >
This PR has been automatically closed due to lack of activity.
- days-before-stale: 45
- days-before-close: 15
+ days-before-stale: 60
+ days-before-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
operations-per-run: 100
remove-stale-when-updated: false
diff --git a/README.md b/README.md
index f1821f78a..877d8b515 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-
+
+

+
NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at
@@ -12,43 +14,34 @@ complete list of requirements, see `requirements.txt`. The code is available [on
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
+| | status |
+|-------------|------------|
+| **master** |  |
+| **develop** |  |
+
+
+
Thank you to our sponsors!
+
+ [](https://ns1.com/)
+
+ [](https://stellar.tech/)
+
+
+
### Discussion
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
-### Build Status
-
-| | status |
-| ----------- | ------------------------------------------------------------------------------------------------- |
-| **master** |  |
-| **develop** |  |
-
-### Screenshots
-
-
-
----
-
-
-
----
-
-
-
----
-
-
-
-## Installation
+### Installation
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`.
-## Providing Feedback
+### Providing Feedback
The best platform for general feedback, assistance, and other discussion is our
[GitHub discussions](https://github.com/netbox-community/netbox/discussions).
@@ -58,7 +51,17 @@ the [appropriate template](https://github.com/netbox-community/netbox/issues/new
If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
-## Related projects
+### Screenshots
+
+
+
+
+
+
+
+
+
+### Related projects
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
for a list of relevant community projects.
diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md
index d8cb671f6..91d5ab2ab 100644
--- a/docs/development/release-checklist.md
+++ b/docs/development/release-checklist.md
@@ -70,7 +70,11 @@ Ensure that continuous integration testing on the `develop` branch is completing
### Update Version and Changelog
-Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. Commit these changes to the `develop` branch.
+* Update the `VERSION` constant in `settings.py` to the new release version.
+* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
+* Replace the "FUTURE" placeholder in the release notes with the current date.
+
+Commit these changes to the `develop` branch.
### Submit a Pull Request
diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md
index 4b5ababbc..0827d5434 100644
--- a/docs/release-notes/version-2.11.md
+++ b/docs/release-notes/version-2.11.md
@@ -1,21 +1,29 @@
# NetBox v2.11
-## v2.11.3 (FUTURE)
+## v2.11.3 (2021-05-07)
### Enhancements
* [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter
* [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type
+* [#6351](https://github.com/netbox-community/netbox/issues/6351) - Add aggregates count to tenant view
+* [#6359](https://github.com/netbox-community/netbox/issues/6359) - Enable custom links for organizational and nested group models
### Bug Fixes
* [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view
* [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view
* [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM
+* [#6312](https://github.com/netbox-community/netbox/issues/6312) - Interface device filter should return all virtual chassis interfaces only if device is master
* [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view
* [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view
* [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key
* [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master
+* [#6350](https://github.com/netbox-community/netbox/issues/6350) - Include first & last IP addresses when allocating available IPv6 addresses via the REST API
+* [#6355](https://github.com/netbox-community/netbox/issues/6355) - Fix caching error when swapping A/Z circuit terminations
+* [#6357](https://github.com/netbox-community/netbox/issues/6357) - Fix ProviderNetwork nested API serializer
+* [#6363](https://github.com/netbox-community/netbox/issues/6363) - Correct pre-population of cluster group when creating a cluster
+* [#6369](https://github.com/netbox-community/netbox/issues/6369) - Fix interface assignment for VLANs in non-scoped groups
---
diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py
index fccf4a8b6..6f7cb4f21 100644
--- a/netbox/circuits/api/nested_serializers.py
+++ b/netbox/circuits/api/nested_serializers.py
@@ -20,7 +20,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta:
- model = Provider
+ model = ProviderNetwork
fields = ['id', 'url', 'display', 'name']
diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py
index b2ffb3c09..31d08537e 100644
--- a/netbox/circuits/models.py
+++ b/netbox/circuits/models.py
@@ -149,7 +149,7 @@ class ProviderNetwork(PrimaryModel):
)
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class CircuitType(OrganizationalModel):
"""
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
diff --git a/netbox/circuits/signals.py b/netbox/circuits/signals.py
index 0a000fb2e..a12cef671 100644
--- a/netbox/circuits/signals.py
+++ b/netbox/circuits/signals.py
@@ -1,9 +1,8 @@
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
-from django.utils import timezone
from dcim.signals import rebuild_paths
-from .models import Circuit, CircuitTermination
+from .models import CircuitTermination
@receiver(post_save, sender=CircuitTermination)
@@ -11,11 +10,9 @@ def update_circuit(instance, **kwargs):
"""
When a CircuitTermination has been modified, update its parent Circuit.
"""
- fields = {
- 'last_updated': timezone.now(),
- f'termination_{instance.term_side.lower()}': instance.pk,
- }
- Circuit.objects.filter(pk=instance.circuit_id).update(**fields)
+ termination_name = f'termination_{instance.term_side.lower()}'
+ setattr(instance.circuit, termination_name, instance)
+ instance.circuit.save()
@receiver((post_save, post_delete), sender=CircuitTermination)
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index 612602316..b4bb0155e 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -211,27 +211,6 @@ class CircuitListView(generic.ObjectListView):
class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all()
- def get_extra_context(self, request, instance):
-
- # A-side termination
- termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
- 'site__region'
- ).filter(
- circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
- ).first()
-
- # Z-side termination
- termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
- 'site__region'
- ).filter(
- circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
- ).first()
-
- return {
- 'termination_a': termination_a,
- 'termination_z': termination_z,
- }
-
class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all()
@@ -296,16 +275,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
if form.is_valid():
- termination_a = CircuitTermination.objects.filter(
- circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
- ).first()
- termination_z = CircuitTermination.objects.filter(
- circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
- ).first()
+ termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first()
+ termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first()
if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
- print('swapping')
with transaction.atomic():
termination_a.term_side = '_'
termination_a.save()
@@ -316,11 +290,20 @@ class CircuitSwapTerminations(generic.ObjectEditView):
elif termination_a:
termination_a.term_side = 'Z'
termination_a.save()
+ circuit.refresh_from_db()
+ circuit.termination_a = None
+ circuit.save()
else:
termination_z.term_side = 'A'
termination_z.save()
+ circuit.refresh_from_db()
+ circuit.termination_z = None
+ circuit.save()
- messages.success(request, "Swapped terminations for circuit {}.".format(circuit))
+ print(f'term A: {circuit.termination_a}')
+ print(f'term Z: {circuit.termination_z}')
+
+ messages.success(request, f"Swapped terminations for circuit {circuit}.")
return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', {
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
index 16e909895..ab8475431 100644
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -2153,7 +2153,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
ip_choices = [(None, '---------')]
# Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
- interface_ids = self.instance.vc_interfaces().values_list('pk', flat=True)
+ interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
# Collect interface IPs
interface_ips = IPAddress.objects.filter(
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 391d6bb4c..ff3da7ca6 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -36,7 +36,7 @@ __all__ = (
# Device Types
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Manufacturer(OrganizationalModel):
"""
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@@ -333,7 +333,7 @@ class DeviceType(PrimaryModel):
# Devices
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceRole(OrganizationalModel):
"""
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@@ -384,7 +384,7 @@ class DeviceRole(OrganizationalModel):
)
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Platform(OrganizationalModel):
"""
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
@@ -718,7 +718,7 @@ class Device(PrimaryModel, ConfigContextModel):
pass
# Validate primary IP addresses
- vc_interfaces = self.vc_interfaces()
+ vc_interfaces = self.vc_interfaces(if_master=False)
if self.primary_ip4:
if self.primary_ip4.family != 4:
raise ValidationError({
@@ -847,9 +847,7 @@ class Device(PrimaryModel, ConfigContextModel):
@property
def interfaces_count(self):
- if self.virtual_chassis and self.virtual_chassis.master == self:
- return self.vc_interfaces().count()
- return self.interfaces.count()
+ return self.vc_interfaces().count()
def get_vc_master(self):
"""
@@ -857,7 +855,7 @@ class Device(PrimaryModel, ConfigContextModel):
"""
return self.virtual_chassis.master if self.virtual_chassis else None
- def vc_interfaces(self, if_master=False):
+ def vc_interfaces(self, if_master=True):
"""
Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
Device belonging to the same VirtualChassis.
@@ -865,7 +863,7 @@ class Device(PrimaryModel, ConfigContextModel):
:param if_master: If True, return VC member interfaces only if this Device is the VC master.
"""
filter = Q(device=self)
- if self.virtual_chassis and (not if_master or self.virtual_chassis.master == self):
+ if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master):
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
return Interface.objects.filter(filter)
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 0c70a9a83..3c63c1a3c 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -35,7 +35,7 @@ __all__ = (
# Racks
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RackRole(OrganizationalModel):
"""
Racks can be organized by functional role, similar to Devices.
diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py
index 225a8e749..1e5165088 100644
--- a/netbox/dcim/models/sites.py
+++ b/netbox/dcim/models/sites.py
@@ -26,7 +26,7 @@ __all__ = (
# Regions
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Region(NestedGroupModel):
"""
A region represents a geographic collection of sites. For example, you might create regions representing countries,
@@ -78,7 +78,7 @@ class Region(NestedGroupModel):
# Site groups
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class SiteGroup(NestedGroupModel):
"""
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
@@ -285,7 +285,7 @@ class Site(PrimaryModel):
# Locations
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Location(NestedGroupModel):
"""
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 734f9bd1a..4ed80d6c8 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -1407,7 +1407,7 @@ class DeviceInterfacesView(generic.ObjectView):
template_name = 'dcim/device/interfaces.html'
def get_extra_context(self, request, instance):
- interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related(
+ interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
'lag', 'cable', '_path__destination', 'tags',
@@ -1529,7 +1529,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
template_name = 'dcim/device/lldp_neighbors.html'
def get_extra_context(self, request, instance):
- interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related(
+ interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
'_path__destination'
).exclude(
type__in=NONCONNECTABLE_IFACE_TYPES
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index b11a88d54..2490a0c5a 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -29,7 +29,7 @@ __all__ = (
)
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RIR(OrganizationalModel):
"""
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
@@ -184,7 +184,7 @@ class Aggregate(PrimaryModel):
return int(float(child_prefixes.size) / self.prefix.size * 100)
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Role(OrganizationalModel):
"""
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
@@ -426,19 +426,11 @@ class Prefix(PrimaryModel):
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
- # All IP addresses within a pool are considered usable
- if self.is_pool:
+ # IPv6, pool, or IPv4 /31 sets are fully usable
+ if self.family == 6 or self.is_pool or self.prefix.prefixlen == 31:
return available_ips
- # All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable
- if (
- self.prefix.version == 4 and self.prefix.prefixlen == 31 # RFC 3021
- ) or (
- self.prefix.version == 6 and self.prefix.prefixlen == 127 # RFC 6164
- ):
- return available_ips
-
- # Omit first and last IP address from the available set
+ # For "normal" IPv4 prefixes, omit first and last addresses
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py
index 616d11aba..b4964e761 100644
--- a/netbox/ipam/models/vlans.py
+++ b/netbox/ipam/models/vlans.py
@@ -21,7 +21,7 @@ __all__ = (
)
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VLANGroup(OrganizationalModel):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py
index 1a723421d..784d58342 100644
--- a/netbox/ipam/querysets.py
+++ b/netbox/ipam/querysets.py
@@ -64,6 +64,7 @@ class VLANQuerySet(RestrictedQuerySet):
return self.filter(
Q(group__in=VLANGroup.objects.filter(q)) |
Q(site=device.site) |
+ Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs
)
@@ -104,6 +105,7 @@ class VLANQuerySet(RestrictedQuerySet):
# Return all applicable VLANs
q = (
Q(group__in=vlan_groups) |
+ Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs
)
if vm.cluster.site:
diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py
index dc3a65747..04a7ed58c 100644
--- a/netbox/secrets/models.py
+++ b/netbox/secrets/models.py
@@ -233,7 +233,7 @@ class SessionKey(BigIDModel):
return session_key
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class SecretRole(OrganizationalModel):
"""
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html
index 752d12424..29af26fc0 100644
--- a/netbox/templates/circuits/circuit.html
+++ b/netbox/templates/circuits/circuit.html
@@ -82,8 +82,8 @@
{% plugin_left_page object %}
- {% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %}
- {% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %}
+ {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
+ {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% plugin_right_page object %}
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html
index 0adc2dab5..9e5391bb2 100644
--- a/netbox/templates/tenancy/tenant.html
+++ b/netbox/templates/tenancy/tenant.html
@@ -78,6 +78,10 @@
VRFs
+
Prefixes
diff --git a/netbox/templates/virtualization/clustergroup.html b/netbox/templates/virtualization/clustergroup.html
index 04f97fd6a..f9a37aaea 100644
--- a/netbox/templates/virtualization/clustergroup.html
+++ b/netbox/templates/virtualization/clustergroup.html
@@ -51,7 +51,7 @@
{% if perms.virtualization.add_cluster %}
diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py
index cad1b3c20..c9f55ec84 100644
--- a/netbox/tenancy/models.py
+++ b/netbox/tenancy/models.py
@@ -14,7 +14,7 @@ __all__ = (
)
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class TenantGroup(NestedGroupModel):
"""
An arbitrary collection of Tenants.
diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py
index 45dffb3c0..b4a29a2e6 100644
--- a/netbox/tenancy/views.py
+++ b/netbox/tenancy/views.py
@@ -1,6 +1,6 @@
from circuits.models import Circuit
from dcim.models import Site, Rack, Device, RackReservation
-from ipam.models import IPAddress, Prefix, VLAN, VRF
+from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from netbox.views import generic
from utilities.tables import paginate_table
from virtualization.models import VirtualMachine, Cluster
@@ -101,6 +101,7 @@ class TenantView(generic.ObjectView):
'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'vrf_count': VRF.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
+ 'aggregate_count': Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index 76f7fe845..5aa43a869 100644
--- a/netbox/virtualization/models.py
+++ b/netbox/virtualization/models.py
@@ -30,7 +30,7 @@ __all__ = (
# Cluster types
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ClusterType(OrganizationalModel):
"""
A type of Cluster.
@@ -73,7 +73,7 @@ class ClusterType(OrganizationalModel):
# Cluster groups
#
-@extras_features('custom_fields', 'export_templates', 'webhooks')
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ClusterGroup(OrganizationalModel):
"""
An organizational group of Clusters.
diff --git a/requirements.txt b/requirements.txt
index a9e000bf3..f7ae8178d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
-Django==3.2
-django-cacheops==5.1
+Django==3.2.2
+django-cacheops==6.0
django-cors-headers==3.7.0
django-debug-toolbar==3.2.1
django-filter==2.4.0