Răsfoiți Sursa

Merge branch 'develop' into feature

jeremystretch 3 ani în urmă
părinte
comite
a74dba865c

+ 4 - 1
.github/workflows/stale.yml

@@ -27,7 +27,10 @@ jobs:
             This issue has been automatically marked as stale because it has not had
             This issue has been automatically marked as stale because it has not had
             recent activity. It will be closed if no further activity occurs. NetBox
             recent activity. It will be closed if no further activity occurs. NetBox
             is governed by a small group of core maintainers which means not all opened
             is governed by a small group of core maintainers which means not all opened
-            issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
+            issues may receive direct feedback. **Do not** attempt to circumvent this
+            process by "bumping" the issue; doing so will result in its immediate closure
+            and you may be barred from participating in any future discussions. Please see
+            our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
           stale-pr-label: 'pending closure'
           stale-pr-label: 'pending closure'
           stale-pr-message: >
           stale-pr-message: >
             This PR has been automatically marked as stale because it has not had
             This PR has been automatically marked as stale because it has not had

+ 3 - 3
CONTRIBUTING.md

@@ -160,9 +160,9 @@ to aid in issue management.
 
 
 It is natural that some new issues get more attention than others. The stale
 It is natural that some new issues get more attention than others. The stale
 bot helps bring renewed attention to potentially valuable issues that may have
 bot helps bring renewed attention to potentially valuable issues that may have
-been overlooked. **Do not** comment on an issue that has been marked stale in
-an effort to circumvent the bot: Doing so will not remove the stale label.
-(Stale labels can be removed only by maintainers.)
+been overlooked. **Do not** comment on a stale issue merely to "bump" it in an
+effort to circumvent the bot: This will result in the immediate closure of the
+issue, and you may be barred from participating in future discussions.
 
 
 ## Maintainer Guidance
 ## Maintainer Guidance
 
 

+ 12 - 0
docs/release-notes/version-3.2.md

@@ -2,6 +2,18 @@
 
 
 ## v3.2.5 (FUTURE)
 ## v3.2.5 (FUTURE)
 
 
+### Enhancements
+
+* [#8882](https://github.com/netbox-community/netbox/issues/8882) - Support filtering IP addresses by multiple parent prefixes
+* [#8893](https://github.com/netbox-community/netbox/issues/8893) - Include count of IP ranges under tenant view
+
+### Bug Fixes
+
+* [#9480](https://github.com/netbox-community/netbox/issues/9480) - Fix sorting services & service templates by port numbers
+* [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
+* [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
+* [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
+
 ---
 ---
 
 
 ## v3.2.4 (2022-05-31)
 ## v3.2.4 (2022-05-31)

+ 9 - 7
netbox/ipam/filtersets.py

@@ -464,7 +464,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
         field_name='address',
         field_name='address',
         lookup_expr='family'
         lookup_expr='family'
     )
     )
-    parent = django_filters.CharFilter(
+    parent = MultiValueCharFilter(
         method='search_by_parent',
         method='search_by_parent',
         label='Parent prefix',
         label='Parent prefix',
     )
     )
@@ -571,14 +571,16 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
     def search_by_parent(self, queryset, name, value):
     def search_by_parent(self, queryset, name, value):
-        value = value.strip()
         if not value:
         if not value:
             return queryset
             return queryset
-        try:
-            query = str(netaddr.IPNetwork(value.strip()).cidr)
-            return queryset.filter(address__net_host_contained=query)
-        except (AddrFormatError, ValueError):
-            return queryset.none()
+        q = Q()
+        for prefix in value:
+            try:
+                query = str(netaddr.IPNetwork(prefix.strip()).cidr)
+                q |= Q(address__net_host_contained=query)
+            except (AddrFormatError, ValueError):
+                return queryset.none()
+        return queryset.filter(q)
 
 
     def filter_address(self, queryset, name, value):
     def filter_address(self, queryset, name, value):
         try:
         try:

+ 4 - 2
netbox/ipam/tables/services.py

@@ -14,7 +14,8 @@ class ServiceTemplateTable(NetBoxTable):
         linkify=True
         linkify=True
     )
     )
     ports = tables.Column(
     ports = tables.Column(
-        accessor=tables.A('port_list')
+        accessor=tables.A('port_list'),
+        order_by=tables.A('ports'),
     )
     )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:servicetemplate_list'
         url_name='ipam:servicetemplate_list'
@@ -35,7 +36,8 @@ class ServiceTable(NetBoxTable):
         order_by=('device', 'virtual_machine')
         order_by=('device', 'virtual_machine')
     )
     )
     ports = tables.Column(
     ports = tables.Column(
-        accessor=tables.A('port_list')
+        accessor=tables.A('port_list'),
+        order_by=tables.A('ports'),
     )
     )
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:service_list'
         url_name='ipam:service_list'

+ 2 - 4
netbox/ipam/tests/test_filtersets.py

@@ -823,10 +823,8 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_parent(self):
     def test_parent(self):
-        params = {'parent': '10.0.0.0/24'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
-        params = {'parent': '2001:db8::/64'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+        params = {'parent': ['10.0.0.0/30', '2001:db8::/126']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
 
 
     def test_filter_address(self):
     def test_filter_address(self):
         # Check IPv4 and IPv6, with and without a mask
         # Check IPv4 and IPv6, with and without a mask

+ 15 - 3
netbox/ipam/views.py

@@ -7,12 +7,12 @@ from django.urls import reverse
 from circuits.models import Provider, Circuit
 from circuits.models import Provider, Circuit
 from circuits.tables import ProviderTable
 from circuits.tables import ProviderTable
 from dcim.filtersets import InterfaceFilterSet
 from dcim.filtersets import InterfaceFilterSet
-from dcim.models import Interface, Site
+from dcim.models import Interface, Site, Device
 from dcim.tables import SiteTable
 from dcim.tables import SiteTable
 from netbox.views import generic
 from netbox.views import generic
 from utilities.utils import count_related
 from utilities.utils import count_related
 from virtualization.filtersets import VMInterfaceFilterSet
 from virtualization.filtersets import VMInterfaceFilterSet
-from virtualization.models import VMInterface
+from virtualization.models import VMInterface, VirtualMachine
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
 from .constants import *
 from .constants import *
 from .models import *
 from .models import *
@@ -676,7 +676,19 @@ class IPAddressView(generic.ObjectView):
         related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
         related_ips_table = tables.IPAddressTable(related_ips, orderable=False)
         related_ips_table.configure(request)
         related_ips_table.configure(request)
 
 
-        services = Service.objects.restrict(request.user, 'view').filter(ipaddresses=instance)
+        # Find services belonging to the IP
+        service_filter = Q(ipaddresses=instance)
+
+        # Find services listening on all IPs on the assigned device/vm
+        if instance.assigned_object and instance.assigned_object.parent_object:
+            parent_object = instance.assigned_object.parent_object
+
+            if isinstance(parent_object, VirtualMachine):
+                service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
+            elif isinstance(parent_object, Device):
+                service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
+
+        services = Service.objects.restrict(request.user, 'view').filter(service_filter)
 
 
         return {
         return {
             'parent_prefixes_table': parent_prefixes_table,
             'parent_prefixes_table': parent_prefixes_table,

+ 7 - 7
netbox/templates/dcim/module.html

@@ -18,25 +18,25 @@
       </button>
       </button>
       <ul class="dropdown-menu" aria-labeled-by="add-components">
       <ul class="dropdown-menu" aria-labeled-by="add-components">
         {% if perms.dcim.add_consoleport %}
         {% if perms.dcim.add_consoleport %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.device.pk %}">Console Ports</a></li>
         {% endif %}
         {% endif %}
         {% if perms.dcim.add_consoleserverport %}
         {% if perms.dcim.add_consoleserverport %}
-          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.device.pk %}">Console Server Ports</a></li>
         {% endif %}
         {% endif %}
         {% if perms.dcim.add_powerport %}
         {% if perms.dcim.add_powerport %}
-          <li><a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.device.pk %}">Power Ports</a></li>
         {% endif %}
         {% endif %}
         {% if perms.dcim.add_poweroutlet %}
         {% if perms.dcim.add_poweroutlet %}
-          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.device.pk %}">Power Outlets</a></li>
         {% endif %}
         {% endif %}
         {% if perms.dcim.add_interface %}
         {% if perms.dcim.add_interface %}
-          <li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.device.pk %}">Interfaces</a></li>
         {% endif %}
         {% endif %}
         {% if perms.dcim.add_frontport %}
         {% if perms.dcim.add_frontport %}
-          <li><a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.device.pk %}">Front Ports</a></li>
         {% endif %}
         {% endif %}
         {% if perms.dcim.add_rearport %}
         {% if perms.dcim.add_rearport %}
-          <li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports</a></li>
+          <li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.device.pk }}&module={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.device.pk %}">Rear Ports</a></li>
         {% endif %}
         {% endif %}
       </ul>
       </ul>
     </div>
     </div>

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

@@ -77,6 +77,10 @@
                     <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>
                     <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>
                     <p>Prefixes</p>
                 </div>
                 </div>
+                <div class="col col-md-4 text-center">
+                    <h2><a href="{% url 'ipam:iprange_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.iprange_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.iprange_count }}</a></h2>
+                    <p>IP Ranges</p>
+                </div>
                 <div class="col col-md-4 text-center">
                 <div class="col col-md-4 text-center">
                     <h2><a href="{% url 'ipam:ipaddress_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.ipaddress_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.ipaddress_count }}</a></h2>
                     <h2><a href="{% url 'ipam:ipaddress_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.ipaddress_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.ipaddress_count }}</a></h2>
                     <p>IP addresses</p>
                     <p>IP addresses</p>

+ 1 - 1
netbox/tenancy/tables/contacts.py

@@ -18,7 +18,7 @@ class ContactGroupTable(NetBoxTable):
     )
     )
     contact_count = columns.LinkedCountColumn(
     contact_count = columns.LinkedCountColumn(
         viewname='tenancy:contact_list',
         viewname='tenancy:contact_list',
-        url_params={'role_id': 'pk'},
+        url_params={'group_id': 'pk'},
         verbose_name='Contacts'
         verbose_name='Contacts'
     )
     )
     tags = columns.TagColumn(
     tags = columns.TagColumn(

+ 3 - 2
netbox/tenancy/views.py

@@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
 from dcim.models import Cable, Device, Location, Rack, RackReservation, Site
 from dcim.models import Cable, Device, Location, Rack, RackReservation, Site
-from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF, ASN
+from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF, ASN
 from netbox.views import generic
 from netbox.views import generic
 from utilities.utils import count_related
 from utilities.utils import count_related
 from virtualization.models import VirtualMachine, Cluster
 from virtualization.models import VirtualMachine, Cluster
@@ -104,8 +104,9 @@ class TenantView(generic.ObjectView):
             'location_count': Location.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'location_count': Location.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'vrf_count': VRF.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(),
             'aggregate_count': Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
+            'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
+            'iprange_count': IPRange.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'ipaddress_count': IPAddress.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(),
             'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
             'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(),