#20285: Collapse consecutive ports into ranges in port_mappings_list
port_mappings_list rendered one token per port, so a service exposing a large contiguous range (e.g. tcp/8000-8100) filled the list column and detail panel with hundreds of tokens. Collapse consecutive ports within a protocol into a range for display, matching the compact form the pre-multi-protocol Service model rendered via array_to_string().
This commit is contained in:
parent
af59d71642
commit
07d92c9501
|
|
@ -7,10 +7,11 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.utils import legacy_protocol_and_ports, split_port_mapping
|
||||
from ipam.utils import group_port_mappings, legacy_protocol_and_ports, split_port_mapping
|
||||
from ipam.validators import validate_port_mappings
|
||||
from netbox.models import PrimaryModel
|
||||
from netbox.models.features import ContactsMixin
|
||||
from utilities.data import array_to_ranges
|
||||
|
||||
__all__ = (
|
||||
'Service',
|
||||
|
|
@ -93,12 +94,22 @@ class ServiceBase(models.Model):
|
|||
@property
|
||||
def port_mappings_list(self):
|
||||
"""
|
||||
Return a user-friendly list of port mappings, e.g. "TCP/80, TCP/443, UDP/53".
|
||||
Return a user-friendly list of port mappings, collapsing consecutive ports within a protocol into
|
||||
a range, e.g. "TCP/80, TCP/443, UDP/53" or "TCP/8000-8100".
|
||||
"""
|
||||
return ', '.join(
|
||||
f'{SERVICE_PROTOCOL_LABELS.get(protocol, protocol)}/{port}'
|
||||
for protocol, port in (split_port_mapping(mapping) for mapping in self.port_mappings)
|
||||
)
|
||||
parts = []
|
||||
for protocol, ports in group_port_mappings(self.port_mappings).items():
|
||||
label = SERVICE_PROTOCOL_LABELS.get(protocol, protocol)
|
||||
int_ports = [int(port) for port in ports if port.isdigit()]
|
||||
for port_range in array_to_ranges(int_ports):
|
||||
if len(port_range) == 1:
|
||||
parts.append(f'{label}/{port_range[0]}')
|
||||
else:
|
||||
parts.append(f'{label}/{port_range[0]}-{port_range[1]}')
|
||||
# A port that isn't a plain integer is only reachable via a write that bypassed validation;
|
||||
# render it verbatim rather than raising, matching sorted_int_ports and normalize_port_mapping.
|
||||
parts.extend(f'{label}/{port}' for port in ports if not port.isdigit())
|
||||
return ', '.join(parts)
|
||||
|
||||
# Read-only legacy accessors mirroring the deprecated REST/GraphQL protocol/ports fields, retained
|
||||
# for backward compatibility with code that read the old single-protocol fields. A multi-protocol
|
||||
|
|
|
|||
|
|
@ -2058,6 +2058,31 @@ class ServiceTestCase(TestCase):
|
|||
)
|
||||
self.assertEqual(service.port_mappings_list, 'TCP/53, UDP/53')
|
||||
|
||||
def test_port_mappings_list_collapses_ranges(self):
|
||||
vm = VirtualMachine.objects.first()
|
||||
|
||||
big = Service.objects.create(
|
||||
name='big',
|
||||
parent=vm,
|
||||
port_mappings=[f'tcp/{port}' for port in range(8000, 8101)],
|
||||
)
|
||||
self.assertEqual(big.port_mappings_list, 'TCP/8000-8100')
|
||||
|
||||
mixed = Service.objects.create(
|
||||
name='mixed',
|
||||
parent=vm,
|
||||
port_mappings=['tcp/82', 'tcp/80', 'tcp/81', 'tcp/443', 'udp/68', 'udp/67'],
|
||||
)
|
||||
self.assertEqual(mixed.port_mappings_list, 'TCP/80-82, TCP/443, UDP/67-68')
|
||||
|
||||
def test_port_mappings_list_tolerates_malformed_ports(self):
|
||||
service = Service.objects.create(
|
||||
name='malformed',
|
||||
parent=VirtualMachine.objects.first(),
|
||||
port_mappings=['tcp/80', 'tcp/abc'],
|
||||
)
|
||||
self.assertEqual(service.port_mappings_list, 'TCP/80, TCP/abc')
|
||||
|
||||
def test_legacy_protocol_ports_properties(self):
|
||||
"""The read-only protocol/ports properties expose the deprecated single-protocol representation."""
|
||||
vm = VirtualMachine.objects.first()
|
||||
|
|
|
|||
Loading…
Reference in New Issue