Procházet zdrojové kódy

Merge branch 'develop' into feature

jeremystretch před 4 roky
rodič
revize
c777daf6e6

+ 1 - 1
.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

+ 1 - 1
.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

+ 2 - 2
.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

+ 29 - 26
README.md

@@ -1,4 +1,6 @@
-![NetBox](docs/netbox_logo.svg "NetBox logo")
+<div align="center">
+  <img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
+</div>
 
 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.
 
-### 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**  | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)  |
+|             | status |
+|-------------|------------|
+| **master**  | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) |
 | **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) |
 
-### Screenshots
+<div align="center">
+  <h4>Thank you to our sponsors!</h4>
 
-![Screenshot of Main Page](docs/media/home-light.png "Main Page")
-
----
-
-![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation")
+  [![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
+  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+  [![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
 
----
+</div>
 
-![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy")
-
----
+### Discussion
 
-![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing")
+* [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
 
-## 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
+
+![Screenshot of Main Page](docs/media/home-light.png "Main Page")
+
+![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation")
+
+![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy")
+
+![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing")
+
+### Related projects
 
 Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
 for a list of relevant community projects.

+ 5 - 1
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
 

+ 9 - 1
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
 
 ---
 

+ 1 - 1
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']
 
 

+ 1 - 1
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

+ 4 - 7
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)

+ 12 - 29
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()
+
+            print(f'term A: {circuit.termination_a}')
+            print(f'term Z: {circuit.termination_z}')
 
-            messages.success(request, "Swapped terminations for circuit {}.".format(circuit))
+            messages.success(request, f"Swapped terminations for circuit {circuit}.")
             return redirect('circuits:circuit', pk=circuit.pk)
 
         return render(request, 'circuits/circuit_terminations_swap.html', {

+ 1 - 1
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(

+ 7 - 9
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)
 

+ 1 - 1
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.

+ 3 - 3
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

+ 2 - 2
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

+ 5 - 13
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:
-            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
-        ):
+        # 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
 
-        # 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),

+ 1 - 1
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.

+ 2 - 0
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:

+ 1 - 1
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

+ 2 - 2
netbox/templates/circuits/circuit.html

@@ -82,8 +82,8 @@
         {% plugin_left_page object %}
 	</div>
 	<div class="col col-md-6">
-        {% 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 %}
     </div>
 </div>

+ 4 - 0
netbox/templates/tenancy/tenant.html

@@ -78,6 +78,10 @@
                     <h2><a href="{% url 'ipam:vrf_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vrf_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
                     <p>VRFs</p>
                 </div>
+                <div class="col col-md-4 text-center">
+                    <h2><a href="{% url 'ipam:aggregate_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.aggregate_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.aggregate_count }}</a></h2>
+                    <p>Aggregates</p>
+                </div>
                 <div class="col col-md-4 text-center">
                     <h2><a href="{% url 'ipam:prefix_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
                     <p>Prefixes</p>

+ 1 - 1
netbox/templates/virtualization/clustergroup.html

@@ -51,7 +51,7 @@
       </div>
       {% if perms.virtualization.add_cluster %}
         <div class="card-footer text-end noprint">
-          <a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary">
+          <a href="{% url 'virtualization:cluster_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
             <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster
           </a>
         </div>

+ 1 - 1
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.

+ 2 - 1
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(),

+ 2 - 2
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.

+ 2 - 2
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