mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-19 10:08:44 -06:00
Compare commits
21 Commits
ad8f3315bc
...
16a9e5b924
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16a9e5b924 | ||
|
|
97b9805b94 | ||
|
|
fe52e4cd74 | ||
|
|
2966288fb5 | ||
|
|
f5ec885c3a | ||
|
|
6cc182c7ce | ||
|
|
c1c912d84b | ||
|
|
7d945b24cb | ||
|
|
697fda1be3 | ||
|
|
144f23444b | ||
|
|
15c32d44e7 | ||
|
|
3140060f21 | ||
|
|
607a385a12 | ||
|
|
834da4e6cd | ||
|
|
8719fd4a54 | ||
|
|
dc09ec3025 | ||
|
|
4e0265a001 | ||
|
|
113c8b7ae6 | ||
|
|
27ffc3df6a | ||
|
|
7bf84eb400 | ||
|
|
e910d461ea |
@@ -38,7 +38,7 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
|
||||
@@ -71,7 +71,7 @@ class CircuitGroupSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count'
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name')
|
||||
@@ -161,7 +161,7 @@ class VirtualCircuitTypeSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'virtual_circuit_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'virtual_circuit_count')
|
||||
|
||||
@@ -353,7 +353,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description',
|
||||
'mark_connected', 'pp_info', 'cable_end', 'cable_position',
|
||||
'mark_connected', 'pp_info', 'cable_end', 'cable_connector',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
||||
@@ -99,7 +99,7 @@ class CircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
nullable_fields = ('color', 'description', 'comments')
|
||||
|
||||
|
||||
class CircuitBulkEditForm(PrimaryModelBulkEditForm):
|
||||
@@ -241,7 +241,7 @@ class CircuitGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
|
||||
model = CircuitGroup
|
||||
nullable_fields = (
|
||||
'description', 'tenant',
|
||||
'description', 'tenant', 'comments',
|
||||
)
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ class VirtualCircuitTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
nullable_fields = ('color', 'description', 'comments')
|
||||
|
||||
|
||||
class VirtualCircuitBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -73,7 +73,7 @@ class CircuitTypeImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class CircuitImportForm(PrimaryModelImportForm):
|
||||
@@ -176,7 +176,7 @@ class CircuitGroupImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'tenant', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||
@@ -199,7 +199,7 @@ class VirtualCircuitTypeImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class VirtualCircuitImportForm(PrimaryModelImportForm):
|
||||
|
||||
@@ -97,7 +97,7 @@ class CircuitTypeForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ class CircuitGroupForm(TenancyForm, OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = CircuitGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ class VirtualCircuitTypeForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = VirtualCircuitType
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
39
netbox/circuits/migrations/0054_cable_connector_positions.py
Normal file
39
netbox/circuits/migrations/0054_cable_connector_positions.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0053_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('circuits', '0053_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0054_cable_connector_positions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuitgroup',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='virtualcircuittype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -20,6 +20,7 @@ class CircuitGroupIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -44,6 +45,7 @@ class CircuitTypeIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -109,5 +111,6 @@ class VirtualCircuitTypeIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -40,8 +40,8 @@ class CircuitTypeTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = CircuitType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
'pk', 'id', 'name', 'circuit_count', 'color', 'description', 'slug', 'comments', 'tags', 'created',
|
||||
'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'circuit_count', 'color', 'description')
|
||||
|
||||
@@ -175,7 +175,7 @@ class CircuitGroupTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = CircuitGroup
|
||||
fields = (
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'tags',
|
||||
'pk', 'name', 'description', 'circuit_group_assignment_count', 'comments', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'description', 'circuit_group_assignment_count')
|
||||
|
||||
@@ -433,7 +433,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = CircuitTerminationFilterSet
|
||||
ignore_fields = ('cable',)
|
||||
ignore_fields = ('cable', 'cable_positions')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -61,11 +61,11 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
model = CableTermination
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id',
|
||||
'termination', 'position', 'created', 'last_updated',
|
||||
'termination', 'positions', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = fields
|
||||
brief_fields = (
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id',
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'positions', 'termination_type', 'termination_id',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,13 +11,15 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
|
||||
|
||||
# Related object counts
|
||||
devicetype_count = RelatedObjectCountField('device_types')
|
||||
moduletype_count = RelatedObjectCountField('module_types')
|
||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||
platform_count = RelatedObjectCountField('platforms')
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'devicetype_count', 'inventoryitem_count', 'platform_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
|
||||
'platform_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
|
||||
|
||||
@@ -30,7 +30,7 @@ class RackRoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'rack_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||
|
||||
@@ -38,7 +38,7 @@ class InventoryItemRoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'inventoryitem_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
|
||||
|
||||
@@ -1,108 +1,382 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.choices import CableEndChoices
|
||||
from dcim.models import CableTermination
|
||||
|
||||
|
||||
class BaseCableProfile:
|
||||
# Maximum number of terminations allowed per side
|
||||
a_max_connections = None
|
||||
b_max_connections = None
|
||||
"""Base class for representing a cable profile."""
|
||||
|
||||
# Mappings of connectors to the number of positions presented by each, at either end of the cable. For example, a
|
||||
# 12-strand MPO fiber cable would have one connector at either end with six positions (six bidirectional fiber
|
||||
# pairs).
|
||||
a_connectors = {}
|
||||
b_connectors = {}
|
||||
|
||||
# Defined a mapping of A/B connector & position pairings. If not defined, all positions are presumed to be
|
||||
# symmetrical (i.e. 1:1 on side A maps to 1:1 on side B). If defined, it must be constructed as a dictionary of
|
||||
# two-item tuples, e.g. {(1, 1): (1, 1)}.
|
||||
_mapping = None
|
||||
|
||||
def clean(self, cable):
|
||||
# Enforce maximum connection limits
|
||||
if self.a_max_connections and len(cable.a_terminations) > self.a_max_connections:
|
||||
# Enforce maximum terminations limits
|
||||
a_terminations_count = len(cable.a_terminations)
|
||||
b_terminations_count = len(cable.b_terminations)
|
||||
max_a_terminations = len(self.a_connectors)
|
||||
max_b_terminations = len(self.b_connectors)
|
||||
if a_terminations_count > max_a_terminations:
|
||||
raise ValidationError({
|
||||
'a_terminations': _(
|
||||
'Maximum A side connections for profile {profile}: {max}'
|
||||
'A side of cable has {count} terminations but only {max} are permitted for profile {profile}'
|
||||
).format(
|
||||
count=a_terminations_count,
|
||||
profile=cable.get_profile_display(),
|
||||
max=self.a_max_connections,
|
||||
max=max_a_terminations,
|
||||
)
|
||||
})
|
||||
if self.b_max_connections and len(cable.b_terminations) > self.b_max_connections:
|
||||
if b_terminations_count > max_b_terminations:
|
||||
raise ValidationError({
|
||||
'b_terminations': _(
|
||||
'Maximum B side connections for profile {profile}: {max}'
|
||||
'B side of cable has {count} terminations but only {max} are permitted for profile {profile}'
|
||||
).format(
|
||||
count=b_terminations_count,
|
||||
profile=cable.get_profile_display(),
|
||||
max=self.b_max_connections,
|
||||
max=max_b_terminations,
|
||||
)
|
||||
})
|
||||
|
||||
def get_mapped_position(self, side, position):
|
||||
def get_mapped_position(self, side, connector, position):
|
||||
"""
|
||||
Return the mapped position for a given cable end and position.
|
||||
|
||||
By default, assume all positions are symmetrical.
|
||||
Return the mapped far-end connector & position for a given cable end the local connector & position.
|
||||
"""
|
||||
return position
|
||||
# By default, assume all positions are symmetrical.
|
||||
if self._mapping:
|
||||
return self._mapping.get((connector, position))
|
||||
return connector, position
|
||||
|
||||
def get_peer_terminations(self, terminations, position_stack):
|
||||
local_end = terminations[0].cable_end
|
||||
qs = CableTermination.objects.filter(
|
||||
cable=terminations[0].cable,
|
||||
cable_end=terminations[0].opposite_cable_end
|
||||
def get_peer_termination(self, termination, position):
|
||||
"""
|
||||
Given a terminating object, return the peer terminating object (if any) on the opposite end of the cable.
|
||||
"""
|
||||
connector, position = self.get_mapped_position(
|
||||
termination.cable_end,
|
||||
termination.cable_connector,
|
||||
position
|
||||
)
|
||||
|
||||
# TODO: Optimize this to use a single query under any condition
|
||||
if position_stack:
|
||||
# Attempt to find a peer termination at the same position currently in the stack. Pop the stack only if
|
||||
# we find one. Otherwise, return any peer terminations with a null position.
|
||||
position = self.get_mapped_position(local_end, position_stack[-1][0])
|
||||
if peers := qs.filter(position=position):
|
||||
position_stack.pop()
|
||||
return peers
|
||||
|
||||
return qs.filter(position=None)
|
||||
try:
|
||||
ct = CableTermination.objects.get(
|
||||
cable=termination.cable,
|
||||
cable_end=termination.opposite_cable_end,
|
||||
connector=connector,
|
||||
positions__contains=[position],
|
||||
)
|
||||
return ct.termination, position
|
||||
except CableTermination.DoesNotExist:
|
||||
return None, None
|
||||
|
||||
|
||||
class StraightSingleCableProfile(BaseCableProfile):
|
||||
a_max_connections = 1
|
||||
b_max_connections = 1
|
||||
# Profile naming:
|
||||
# - Single: One connector per side, with one or more positions
|
||||
# - Trunk: Two or more connectors per side, with one or more positions per connector
|
||||
# - Breakout: One or more connectors on the A side which map to a greater number of B side connectors
|
||||
# - Shuffle: A cable with nonlinear position mappings between sides
|
||||
|
||||
|
||||
class StraightMultiCableProfile(BaseCableProfile):
|
||||
a_max_connections = None
|
||||
b_max_connections = None
|
||||
|
||||
|
||||
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
|
||||
a_max_connections = 8
|
||||
b_max_connections = 8
|
||||
_mapping = {
|
||||
class Single1C1PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 1,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Single1C2PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 2,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Single1C4PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Single1C6PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 6,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Single1C8PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 8,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Single1C12PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 12,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Single1C16PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 16,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk2C1PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 1,
|
||||
2: 1,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk2C2PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 2,
|
||||
2: 2,
|
||||
3: 5,
|
||||
4: 6,
|
||||
5: 3,
|
||||
6: 4,
|
||||
7: 7,
|
||||
8: 8,
|
||||
}
|
||||
|
||||
def get_mapped_position(self, side, position):
|
||||
return self._mapping.get(position)
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Shuffle4x4MPO8CableProfile(BaseCableProfile):
|
||||
a_max_connections = 8
|
||||
b_max_connections = 8
|
||||
# A side to B side position mapping
|
||||
_a_mapping = {
|
||||
class Trunk2C4PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
2: 4,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk2C6PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 6,
|
||||
2: 6,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk2C8PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 8,
|
||||
2: 8,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk2C12PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 12,
|
||||
2: 12,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk4C1PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 1,
|
||||
2: 3,
|
||||
3: 5,
|
||||
4: 7,
|
||||
5: 2,
|
||||
6: 4,
|
||||
7: 6,
|
||||
8: 8,
|
||||
2: 1,
|
||||
3: 1,
|
||||
4: 1,
|
||||
}
|
||||
# B side to A side position mapping (reverse of _a_mapping)
|
||||
_b_mapping = {v: k for k, v in _a_mapping.items()}
|
||||
b_connectors = a_connectors
|
||||
|
||||
def get_mapped_position(self, side, position):
|
||||
if side.lower() == 'b':
|
||||
return self._b_mapping.get(position)
|
||||
return self._a_mapping.get(position)
|
||||
|
||||
class Trunk4C2PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 2,
|
||||
2: 2,
|
||||
3: 2,
|
||||
4: 2,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk4C4PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
2: 4,
|
||||
3: 4,
|
||||
4: 4,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk4C6PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 6,
|
||||
2: 6,
|
||||
3: 6,
|
||||
4: 6,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk4C8PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 8,
|
||||
2: 8,
|
||||
3: 8,
|
||||
4: 8,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Trunk8C4PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
2: 4,
|
||||
3: 4,
|
||||
4: 4,
|
||||
5: 4,
|
||||
6: 4,
|
||||
7: 4,
|
||||
8: 4,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
|
||||
|
||||
class Breakout1C4Px4C1PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
}
|
||||
b_connectors = {
|
||||
1: 1,
|
||||
2: 1,
|
||||
3: 1,
|
||||
4: 1,
|
||||
}
|
||||
_mapping = {
|
||||
(1, 1): (1, 1),
|
||||
(1, 2): (2, 1),
|
||||
(1, 3): (3, 1),
|
||||
(1, 4): (4, 1),
|
||||
(2, 1): (1, 2),
|
||||
(3, 1): (1, 3),
|
||||
(4, 1): (1, 4),
|
||||
}
|
||||
|
||||
|
||||
class Breakout1C6Px6C1PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 6,
|
||||
}
|
||||
b_connectors = {
|
||||
1: 1,
|
||||
2: 1,
|
||||
3: 1,
|
||||
4: 1,
|
||||
5: 1,
|
||||
6: 1,
|
||||
}
|
||||
_mapping = {
|
||||
(1, 1): (1, 1),
|
||||
(1, 2): (2, 1),
|
||||
(1, 3): (3, 1),
|
||||
(1, 4): (4, 1),
|
||||
(1, 5): (5, 1),
|
||||
(1, 6): (6, 1),
|
||||
(2, 1): (1, 2),
|
||||
(3, 1): (1, 3),
|
||||
(4, 1): (1, 4),
|
||||
(5, 1): (1, 5),
|
||||
(6, 1): (1, 6),
|
||||
}
|
||||
|
||||
|
||||
class Shuffle2C4PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
2: 4,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
_mapping = {
|
||||
(1, 1): (1, 1),
|
||||
(1, 2): (1, 2),
|
||||
(1, 3): (2, 1),
|
||||
(1, 4): (2, 2),
|
||||
(2, 1): (1, 3),
|
||||
(2, 2): (1, 4),
|
||||
(2, 3): (2, 3),
|
||||
(2, 4): (2, 4),
|
||||
}
|
||||
|
||||
|
||||
class Shuffle4C4PCableProfile(BaseCableProfile):
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
2: 4,
|
||||
3: 4,
|
||||
4: 4,
|
||||
}
|
||||
b_connectors = a_connectors
|
||||
_mapping = {
|
||||
(1, 1): (1, 1),
|
||||
(1, 2): (2, 1),
|
||||
(1, 3): (3, 1),
|
||||
(1, 4): (4, 1),
|
||||
(2, 1): (1, 2),
|
||||
(2, 2): (2, 2),
|
||||
(2, 3): (3, 2),
|
||||
(2, 4): (4, 2),
|
||||
(3, 1): (1, 3),
|
||||
(3, 2): (2, 3),
|
||||
(3, 3): (3, 3),
|
||||
(3, 4): (4, 3),
|
||||
(4, 1): (1, 4),
|
||||
(4, 2): (2, 4),
|
||||
(4, 3): (3, 4),
|
||||
(4, 4): (4, 4),
|
||||
}
|
||||
|
||||
|
||||
class ShuffleBreakout2x8CableProfile(BaseCableProfile):
|
||||
"""
|
||||
Temporary solution for mapping 2 front/rear ports to 8 discrete interfaces
|
||||
"""
|
||||
a_connectors = {
|
||||
1: 4,
|
||||
2: 4,
|
||||
}
|
||||
b_connectors = {
|
||||
1: 1,
|
||||
2: 1,
|
||||
3: 1,
|
||||
4: 1,
|
||||
5: 1,
|
||||
6: 1,
|
||||
7: 1,
|
||||
8: 1,
|
||||
}
|
||||
_a_mapping = {
|
||||
(1, 1): (1, 1),
|
||||
(1, 2): (2, 1),
|
||||
(1, 3): (5, 1),
|
||||
(1, 4): (6, 1),
|
||||
(2, 1): (3, 1),
|
||||
(2, 2): (4, 1),
|
||||
(2, 3): (7, 1),
|
||||
(2, 4): (8, 1),
|
||||
}
|
||||
_b_mapping = {
|
||||
(1, 1): (1, 1),
|
||||
(2, 1): (1, 2),
|
||||
(3, 1): (2, 1),
|
||||
(4, 1): (2, 2),
|
||||
(5, 1): (1, 3),
|
||||
(6, 1): (1, 4),
|
||||
(7, 1): (2, 3),
|
||||
(8, 1): (2, 4),
|
||||
}
|
||||
|
||||
def get_mapped_position(self, side, connector, position):
|
||||
if side.lower() == CableEndChoices.SIDE_A:
|
||||
return self._a_mapping.get((connector, position))
|
||||
return self._b_mapping.get((connector, position))
|
||||
|
||||
@@ -1722,16 +1722,80 @@ class PortTypeChoices(ChoiceSet):
|
||||
#
|
||||
|
||||
class CableProfileChoices(ChoiceSet):
|
||||
STRAIGHT_SINGLE = 'straight-single'
|
||||
STRAIGHT_MULTI = 'straight-multi'
|
||||
SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8'
|
||||
SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8'
|
||||
# Singles
|
||||
SINGLE_1C1P = 'single-1c1p'
|
||||
SINGLE_1C2P = 'single-1c2p'
|
||||
SINGLE_1C4P = 'single-1c4p'
|
||||
SINGLE_1C6P = 'single-1c6p'
|
||||
SINGLE_1C8P = 'single-1c8p'
|
||||
SINGLE_1C12P = 'single-1c12p'
|
||||
SINGLE_1C16P = 'single-1c16p'
|
||||
# Trunks
|
||||
TRUNK_2C1P = 'trunk-2c1p'
|
||||
TRUNK_2C2P = 'trunk-2c2p'
|
||||
TRUNK_2C4P = 'trunk-2c4p'
|
||||
TRUNK_2C6P = 'trunk-2c6p'
|
||||
TRUNK_2C8P = 'trunk-2c8p'
|
||||
TRUNK_2C12P = 'trunk-2c12p'
|
||||
TRUNK_4C1P = 'trunk-4c1p'
|
||||
TRUNK_4C2P = 'trunk-4c2p'
|
||||
TRUNK_4C4P = 'trunk-4c4p'
|
||||
TRUNK_4C6P = 'trunk-4c6p'
|
||||
TRUNK_4C8P = 'trunk-4c8p'
|
||||
TRUNK_8C4P = 'trunk-8c4p'
|
||||
# Breakouts
|
||||
BREAKOUT_1C4P_4C1P = 'breakout-1c4p-4c1p'
|
||||
BREAKOUT_1C6P_6C1P = 'breakout-1c6p-6c1p'
|
||||
SHUFFLE_BREAKOUT_2X8 = 'shuffle-breakout-2x8'
|
||||
# Shuffles
|
||||
SHUFFLE_2C4P = 'shuffle-2c4p'
|
||||
SHUFFLE_4C4P = 'shuffle-4c4p'
|
||||
|
||||
CHOICES = (
|
||||
(STRAIGHT_SINGLE, _('Straight (single position)')),
|
||||
(STRAIGHT_MULTI, _('Straight (multi-position)')),
|
||||
(SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')),
|
||||
(SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')),
|
||||
(
|
||||
_('Single'),
|
||||
(
|
||||
(SINGLE_1C1P, _('Single (1C1P)')),
|
||||
(SINGLE_1C2P, _('Single (1C2P)')),
|
||||
(SINGLE_1C4P, _('Single (1C4P)')),
|
||||
(SINGLE_1C6P, _('Single (1C6P)')),
|
||||
(SINGLE_1C8P, _('Single (1C8P)')),
|
||||
(SINGLE_1C12P, _('Single (1C12P)')),
|
||||
(SINGLE_1C16P, _('Single (1C16P)')),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('Trunk'),
|
||||
(
|
||||
(TRUNK_2C1P, _('Trunk (2C1P)')),
|
||||
(TRUNK_2C2P, _('Trunk (2C2P)')),
|
||||
(TRUNK_2C4P, _('Trunk (2C4P)')),
|
||||
(TRUNK_2C6P, _('Trunk (2C6P)')),
|
||||
(TRUNK_2C8P, _('Trunk (2C8P)')),
|
||||
(TRUNK_2C12P, _('Trunk (2C12P)')),
|
||||
(TRUNK_4C1P, _('Trunk (4C1P)')),
|
||||
(TRUNK_4C2P, _('Trunk (4C2P)')),
|
||||
(TRUNK_4C4P, _('Trunk (4C4P)')),
|
||||
(TRUNK_4C6P, _('Trunk (4C6P)')),
|
||||
(TRUNK_4C8P, _('Trunk (4C8P)')),
|
||||
(TRUNK_8C4P, _('Trunk (8C4P)')),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('Breakout'),
|
||||
(
|
||||
(BREAKOUT_1C4P_4C1P, _('Breakout (1C4P/4C1P)')),
|
||||
(BREAKOUT_1C6P_6C1P, _('Breakout (1C6P/6C1P)')),
|
||||
),
|
||||
),
|
||||
(
|
||||
_('Shuffle'),
|
||||
(
|
||||
(SHUFFLE_2C4P, _('Shuffle (2C4P)')),
|
||||
(SHUFFLE_4C4P, _('Shuffle (4C4P)')),
|
||||
(SHUFFLE_BREAKOUT_2X8, _('Shuffle breakout (2x8)')),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ RACK_STARTING_UNIT_DEFAULT = 1
|
||||
# Cables
|
||||
#
|
||||
|
||||
CABLE_CONNECTOR_MIN = 1
|
||||
CABLE_CONNECTOR_MAX = 256
|
||||
|
||||
CABLE_POSITION_MIN = 1
|
||||
CABLE_POSITION_MAX = 1024
|
||||
|
||||
|
||||
@@ -1748,7 +1748,9 @@ class ConsolePortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||
fields = (
|
||||
'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector',
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
@@ -1760,7 +1762,9 @@ class ConsoleServerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFi
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_position')
|
||||
fields = (
|
||||
'id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end', 'cable_connector',
|
||||
)
|
||||
|
||||
|
||||
@register_filterset
|
||||
@@ -1774,7 +1778,7 @@ class PowerPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet,
|
||||
model = PowerPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
'cable_connector',
|
||||
)
|
||||
|
||||
|
||||
@@ -1801,7 +1805,7 @@ class PowerOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
|
||||
model = PowerOutlet
|
||||
fields = (
|
||||
'id', 'name', 'status', 'label', 'feed_leg', 'description', 'color', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
'cable_connector',
|
||||
)
|
||||
|
||||
|
||||
@@ -2111,7 +2115,7 @@ class InterfaceFilterSet(
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
|
||||
'cable_id', 'cable_end', 'cable_position',
|
||||
'cable_id', 'cable_end', 'cable_connector',
|
||||
)
|
||||
|
||||
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
|
||||
@@ -2167,7 +2171,7 @@ class FrontPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet)
|
||||
model = FrontPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
'cable_connector',
|
||||
)
|
||||
|
||||
|
||||
@@ -2188,7 +2192,7 @@ class RearPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSet):
|
||||
model = RearPort
|
||||
fields = (
|
||||
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
|
||||
'cable_position',
|
||||
'cable_connector',
|
||||
)
|
||||
|
||||
|
||||
@@ -2544,7 +2548,7 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CableTermination
|
||||
fields = ('id', 'cable', 'cable_end', 'position', 'termination_type', 'termination_id')
|
||||
fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
|
||||
|
||||
|
||||
@register_filterset
|
||||
@@ -2663,7 +2667,7 @@ class PowerFeedFilterSet(PrimaryModelFilterSet, CabledObjectFilterSet, PathEndpo
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
|
||||
'available_power', 'mark_connected', 'cable_end', 'cable_position', 'description',
|
||||
'available_power', 'mark_connected', 'cable_end', 'cable_connector', 'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
||||
@@ -208,7 +208,7 @@ class RackRoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
nullable_fields = ('color', 'description', 'comments')
|
||||
|
||||
|
||||
class RackTypeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
@@ -474,7 +474,7 @@ class ManufacturerBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
@@ -1719,7 +1719,7 @@ class InventoryItemRoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('color', 'description'),
|
||||
)
|
||||
nullable_fields = ('color', 'description')
|
||||
nullable_fields = ('color', 'description', 'comments')
|
||||
|
||||
|
||||
class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -183,7 +183,7 @@ class RackRoleImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class RackTypeImportForm(PrimaryModelImportForm):
|
||||
@@ -400,7 +400,7 @@ class ManufacturerImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class DeviceTypeImportForm(PrimaryModelImportForm):
|
||||
@@ -1298,7 +1298,7 @@ class InventoryItemRoleImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = ('name', 'slug', 'color', 'description')
|
||||
fields = ('name', 'slug', 'color', 'description', 'owner', 'comments')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -202,7 +202,7 @@ class RackRoleForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -345,7 +345,7 @@ class ManufacturerForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -1880,7 +1880,7 @@ class InventoryItemRoleForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = [
|
||||
'name', 'slug', 'color', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -16,25 +17,40 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cabletermination',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(
|
||||
name='connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
]
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cabletermination',
|
||||
name='positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024)
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cabletermination',
|
||||
options={'ordering': ('cable', 'cable_end', 'position', 'pk')},
|
||||
options={'ordering': ('cable', 'cable_end', 'connector', 'positions', 'pk')},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='cabletermination',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=('cable', 'cable_end', 'position'),
|
||||
name='dcim_cabletermination_unique_position'
|
||||
fields=('cable', 'cable_end', 'connector'),
|
||||
name='dcim_cabletermination_unique_connector'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
228
netbox/dcim/migrations/0221_cable_connector_positions.py
Normal file
228
netbox/dcim/migrations/0221_cable_connector_positions.py
Normal file
@@ -0,0 +1,228 @@
|
||||
import django.contrib.postgres.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0220_cable_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='cable_connector',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(256)
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='cable_positions',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
]
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,107 +0,0 @@
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0220_cable_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='cable_position',
|
||||
field=models.PositiveIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(1024),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -59,7 +59,7 @@ def populate_port_mappings(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0221_cable_position'),
|
||||
('dcim', '0221_cable_connector_positions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0223_frontport_positions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventoryitemrole',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='manufacturer',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rackrole',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
@@ -136,10 +137,30 @@ class Cable(PrimaryModel):
|
||||
def profile_class(self):
|
||||
from dcim import cable_profiles
|
||||
return {
|
||||
CableProfileChoices.STRAIGHT_SINGLE: cable_profiles.StraightSingleCableProfile,
|
||||
CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile,
|
||||
CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile,
|
||||
CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile,
|
||||
CableProfileChoices.SINGLE_1C1P: cable_profiles.Single1C1PCableProfile,
|
||||
CableProfileChoices.SINGLE_1C2P: cable_profiles.Single1C2PCableProfile,
|
||||
CableProfileChoices.SINGLE_1C4P: cable_profiles.Single1C4PCableProfile,
|
||||
CableProfileChoices.SINGLE_1C6P: cable_profiles.Single1C6PCableProfile,
|
||||
CableProfileChoices.SINGLE_1C8P: cable_profiles.Single1C8PCableProfile,
|
||||
CableProfileChoices.SINGLE_1C12P: cable_profiles.Single1C12PCableProfile,
|
||||
CableProfileChoices.SINGLE_1C16P: cable_profiles.Single1C16PCableProfile,
|
||||
CableProfileChoices.TRUNK_2C1P: cable_profiles.Trunk2C1PCableProfile,
|
||||
CableProfileChoices.TRUNK_2C2P: cable_profiles.Trunk2C2PCableProfile,
|
||||
CableProfileChoices.TRUNK_2C4P: cable_profiles.Trunk2C4PCableProfile,
|
||||
CableProfileChoices.TRUNK_2C6P: cable_profiles.Trunk2C6PCableProfile,
|
||||
CableProfileChoices.TRUNK_2C8P: cable_profiles.Trunk2C8PCableProfile,
|
||||
CableProfileChoices.TRUNK_2C12P: cable_profiles.Trunk2C12PCableProfile,
|
||||
CableProfileChoices.TRUNK_4C1P: cable_profiles.Trunk4C1PCableProfile,
|
||||
CableProfileChoices.TRUNK_4C2P: cable_profiles.Trunk4C2PCableProfile,
|
||||
CableProfileChoices.TRUNK_4C4P: cable_profiles.Trunk4C4PCableProfile,
|
||||
CableProfileChoices.TRUNK_4C6P: cable_profiles.Trunk4C6PCableProfile,
|
||||
CableProfileChoices.TRUNK_4C8P: cable_profiles.Trunk4C8PCableProfile,
|
||||
CableProfileChoices.TRUNK_8C4P: cable_profiles.Trunk8C4PCableProfile,
|
||||
CableProfileChoices.BREAKOUT_1C4P_4C1P: cable_profiles.Breakout1C4Px4C1PCableProfile,
|
||||
CableProfileChoices.BREAKOUT_1C6P_6C1P: cable_profiles.Breakout1C6Px6C1PCableProfile,
|
||||
CableProfileChoices.SHUFFLE_2C4P: cable_profiles.Shuffle2C4PCableProfile,
|
||||
CableProfileChoices.SHUFFLE_4C4P: cable_profiles.Shuffle4C4PCableProfile,
|
||||
CableProfileChoices.SHUFFLE_BREAKOUT_2X8: cable_profiles.ShuffleBreakout2x8CableProfile,
|
||||
}.get(self.profile)
|
||||
|
||||
def _get_x_terminations(self, side):
|
||||
@@ -340,12 +361,30 @@ class Cable(PrimaryModel):
|
||||
# Save any new CableTerminations
|
||||
for i, termination in enumerate(self.a_terminations, start=1):
|
||||
if not termination.pk or termination not in a_terminations:
|
||||
position = i if self.profile and isinstance(termination, PathEndpoint) else None
|
||||
CableTermination(cable=self, cable_end='A', position=position, termination=termination).save()
|
||||
connector = positions = None
|
||||
if self.profile:
|
||||
connector = i
|
||||
positions = list(range(1, self.profile_class().a_connectors[i] + 1))
|
||||
CableTermination(
|
||||
cable=self,
|
||||
cable_end='A',
|
||||
connector=connector,
|
||||
positions=positions,
|
||||
termination=termination
|
||||
).save()
|
||||
for i, termination in enumerate(self.b_terminations, start=1):
|
||||
if not termination.pk or termination not in b_terminations:
|
||||
position = i if self.profile and isinstance(termination, PathEndpoint) else None
|
||||
CableTermination(cable=self, cable_end='B', position=position, termination=termination).save()
|
||||
connector = positions = None
|
||||
if self.profile:
|
||||
connector = i
|
||||
positions = list(range(1, self.profile_class().b_connectors[i] + 1))
|
||||
CableTermination(
|
||||
cable=self,
|
||||
cable_end='B',
|
||||
connector=connector,
|
||||
positions=positions,
|
||||
termination=termination
|
||||
).save()
|
||||
|
||||
|
||||
class CableTermination(ChangeLoggedModel):
|
||||
@@ -372,13 +411,23 @@ class CableTermination(ChangeLoggedModel):
|
||||
ct_field='termination_type',
|
||||
fk_field='termination_id'
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
connector = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=(
|
||||
MinValueValidator(CABLE_POSITION_MIN),
|
||||
MaxValueValidator(CABLE_POSITION_MAX)
|
||||
)
|
||||
MinValueValidator(CABLE_CONNECTOR_MIN),
|
||||
MaxValueValidator(CABLE_CONNECTOR_MAX)
|
||||
),
|
||||
)
|
||||
positions = ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=(
|
||||
MinValueValidator(CABLE_POSITION_MIN),
|
||||
MaxValueValidator(CABLE_POSITION_MAX)
|
||||
)
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
# Cached associations to enable efficient filtering
|
||||
@@ -410,15 +459,15 @@ class CableTermination(ChangeLoggedModel):
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('cable', 'cable_end', 'position', 'pk')
|
||||
ordering = ('cable', 'cable_end', 'connector', 'positions', 'pk')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('termination_type', 'termination_id'),
|
||||
name='%(app_label)s_%(class)s_unique_termination'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('cable', 'cable_end', 'position'),
|
||||
name='%(app_label)s_%(class)s_unique_position'
|
||||
fields=('cable', 'cable_end', 'connector'),
|
||||
name='%(app_label)s_%(class)s_unique_connector'
|
||||
),
|
||||
)
|
||||
verbose_name = _('cable termination')
|
||||
@@ -483,7 +532,8 @@ class CableTermination(ChangeLoggedModel):
|
||||
termination.snapshot()
|
||||
termination.cable = self.cable
|
||||
termination.cable_end = self.cable_end
|
||||
termination.cable_position = self.position
|
||||
termination.cable_connector = self.connector
|
||||
termination.cable_positions = self.positions
|
||||
termination.save()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
@@ -493,7 +543,8 @@ class CableTermination(ChangeLoggedModel):
|
||||
termination.snapshot()
|
||||
termination.cable = None
|
||||
termination.cable_end = None
|
||||
termination.cable_position = None
|
||||
termination.cable_connector = None
|
||||
termination.cable_positions = None
|
||||
termination.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -701,9 +752,10 @@ class CablePath(models.Model):
|
||||
path.append([
|
||||
object_to_path_node(t) for t in terminations
|
||||
])
|
||||
# If not null, push cable_position onto the stack
|
||||
if terminations[0].cable_position is not None:
|
||||
position_stack.append([terminations[0].cable_position])
|
||||
# If not null, push cable position onto the stack
|
||||
# TODO: Handle multiple positions?
|
||||
if isinstance(terminations[0], PathEndpoint) and terminations[0].cable_positions:
|
||||
position_stack.append([terminations[0].cable_positions[0]])
|
||||
|
||||
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
||||
links = list(dict.fromkeys(
|
||||
@@ -744,8 +796,9 @@ class CablePath(models.Model):
|
||||
# Profile-based tracing
|
||||
if links[0].profile:
|
||||
cable_profile = links[0].profile_class()
|
||||
peer_cable_terminations = cable_profile.get_peer_terminations(terminations, position_stack)
|
||||
remote_terminations = [ct.termination for ct in peer_cable_terminations]
|
||||
term, position = cable_profile.get_peer_termination(terminations[0], position_stack.pop()[0])
|
||||
remote_terminations = [term]
|
||||
position_stack.append([position])
|
||||
|
||||
# Legacy (positionless) behavior
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from functools import cached_property
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
@@ -177,15 +178,24 @@ class CabledObjectModel(models.Model):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
cable_position = models.PositiveIntegerField(
|
||||
verbose_name=_('cable position'),
|
||||
cable_connector = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
validators=(
|
||||
MinValueValidator(CABLE_POSITION_MIN),
|
||||
MaxValueValidator(CABLE_POSITION_MAX)
|
||||
MinValueValidator(CABLE_CONNECTOR_MIN),
|
||||
MaxValueValidator(CABLE_CONNECTOR_MAX)
|
||||
),
|
||||
)
|
||||
cable_positions = ArrayField(
|
||||
base_field=models.PositiveSmallIntegerField(
|
||||
validators=(
|
||||
MinValueValidator(CABLE_POSITION_MIN),
|
||||
MaxValueValidator(CABLE_POSITION_MAX)
|
||||
)
|
||||
),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
mark_connected = models.BooleanField(
|
||||
verbose_name=_('mark connected'),
|
||||
default=False,
|
||||
@@ -210,18 +220,31 @@ class CabledObjectModel(models.Model):
|
||||
raise ValidationError({
|
||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||
})
|
||||
if self.cable_end and not self.cable:
|
||||
raise ValidationError({
|
||||
"cable_end": _("Cable end must not be set without a cable.")
|
||||
})
|
||||
if self.cable_position and not self.cable:
|
||||
raise ValidationError({
|
||||
"cable_position": _("Cable termination position must not be set without a cable.")
|
||||
})
|
||||
if self.mark_connected and self.cable:
|
||||
raise ValidationError({
|
||||
"mark_connected": _("Cannot mark as connected with a cable attached.")
|
||||
})
|
||||
if self.cable_connector and not self.cable_positions:
|
||||
raise ValidationError({
|
||||
"cable_positions": _("Must specify position(s) when specifying a cable connector.")
|
||||
})
|
||||
if self.cable_positions and not self.cable_connector:
|
||||
raise ValidationError({
|
||||
"cable_positions": _("Cable positions cannot be set without a cable connector.")
|
||||
})
|
||||
if self.mark_connected:
|
||||
raise ValidationError({
|
||||
"mark_connected": _("Cannot mark as connected with a cable attached.")
|
||||
})
|
||||
else:
|
||||
if self.cable_end:
|
||||
raise ValidationError({
|
||||
"cable_end": _("Cable end must not be set without a cable.")
|
||||
})
|
||||
if self.cable_connector:
|
||||
raise ValidationError({
|
||||
"cable_connector": _("Cable connector must not be set without a cable.")
|
||||
})
|
||||
if self.cable_positions:
|
||||
raise ValidationError({
|
||||
"cable_positions": _("Cable termination positions must not be set without a cable.")
|
||||
})
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
|
||||
@@ -137,6 +137,18 @@ class InventoryItemIndex(SearchIndex):
|
||||
display_attrs = ('device', 'manufacturer', 'parent', 'part_id', 'serial', 'asset_tag', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class InventoryItemRoleIndex(SearchIndex):
|
||||
model = models.InventoryItemRole
|
||||
fields = (
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@register_search
|
||||
class LocationIndex(SearchIndex):
|
||||
model = models.Location
|
||||
@@ -157,6 +169,7 @@ class ManufacturerIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -308,6 +321,7 @@ class RackRoleIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@@ -1057,7 +1057,7 @@ class InventoryItemRoleTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = models.InventoryItemRole
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'inventoryitem_count', 'color', 'description', 'slug', 'comments', 'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'inventoryitem_count', 'color', 'description')
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ class ManufacturerTable(ContactsColumnMixin, OrganizationalModelTable):
|
||||
model = models.Manufacturer
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
|
||||
'platform_count', 'description', 'slug', 'tags', 'contacts', 'actions', 'created', 'last_updated',
|
||||
'platform_count', 'description', 'slug', 'comments', 'tags', 'contacts', 'actions', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'racktype_count', 'devicetype_count', 'moduletype_count', 'inventoryitem_count',
|
||||
|
||||
@@ -35,7 +35,7 @@ class RackRoleTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = RackRole
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'tags', 'actions', 'created',
|
||||
'pk', 'id', 'name', 'rack_count', 'color', 'description', 'slug', 'comments', 'tags', 'actions', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
|
||||
|
||||
@@ -2586,7 +2586,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
'object_id': interfaces[14].pk,
|
||||
}],
|
||||
'label': 'Cable 4',
|
||||
'profile': CableProfileChoices.STRAIGHT_SINGLE,
|
||||
'profile': CableProfileChoices.SINGLE_1C1P,
|
||||
},
|
||||
{
|
||||
'a_terminations': [{
|
||||
@@ -2598,7 +2598,7 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
'object_id': interfaces[15].pk,
|
||||
}],
|
||||
'label': 'Cable 5',
|
||||
'profile': CableProfileChoices.STRAIGHT_SINGLE,
|
||||
'profile': CableProfileChoices.SINGLE_1C1P,
|
||||
},
|
||||
{
|
||||
'a_terminations': [{
|
||||
@@ -2620,7 +2620,7 @@ class CableTerminationTest(
|
||||
APIViewTestCases.ListObjectsViewTestCase,
|
||||
):
|
||||
model = CableTermination
|
||||
brief_fields = ['cable', 'cable_end', 'display', 'id', 'position', 'termination_id', 'termination_type', 'url']
|
||||
brief_fields = ['cable', 'cable_end', 'display', 'id', 'positions', 'termination_id', 'termination_type', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3332,6 +3332,7 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = ConsolePortFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -3582,6 +3583,7 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = ConsoleServerPortFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -3832,6 +3834,7 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
||||
class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = PowerPortFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -4096,6 +4099,7 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = PowerOutletFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -4380,7 +4384,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = InterfaceFilterSet
|
||||
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs')
|
||||
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'qinq_svlan', 'vdcs', 'cable_positions')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -5017,6 +5021,7 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = FrontPortFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -5321,6 +5326,7 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = RearPortFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -6859,6 +6865,7 @@ class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = PowerFeedFilterSet
|
||||
ignore_fields = ('cable_positions',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -41,12 +41,12 @@ def create_cablepaths(objects):
|
||||
"""
|
||||
from dcim.models import CablePath
|
||||
|
||||
# Arrange objects by cable position. All objects with a null position are grouped together.
|
||||
# Arrange objects by cable connector. All objects with a null connector are grouped together.
|
||||
origins = defaultdict(list)
|
||||
for obj in objects:
|
||||
origins[obj.cable_position].append(obj)
|
||||
origins[obj.cable_connector].append(obj)
|
||||
|
||||
for position, objects in origins.items():
|
||||
for connector, objects in origins.items():
|
||||
if cp := CablePath.from_origin(objects):
|
||||
cp.save()
|
||||
|
||||
|
||||
@@ -799,6 +799,7 @@ class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1233,7 +1234,7 @@ class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Manufacturer.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[OrganizationalObjectPanel(), TagsPanel()],
|
||||
right_panels=[RelatedObjectsPanel(), CustomFieldsPanel()],
|
||||
right_panels=[RelatedObjectsPanel(), CustomFieldsPanel(), CommentsPanel()],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
||||
@@ -21,8 +21,8 @@ class RIRSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'is_private', 'description', 'owner', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'aggregate_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'is_private', 'description', 'owner', 'comments',
|
||||
'tags', 'custom_fields', 'created', 'last_updated', 'aggregate_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
|
||||
|
||||
@@ -36,7 +36,7 @@ class ASNRangeSerializer(OrganizationalModelSerializer):
|
||||
model = ASNRange
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description',
|
||||
'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'asn_count',
|
||||
'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'asn_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class RoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'owner', 'tags',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'prefix_count', 'vlan_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count')
|
||||
|
||||
@@ -45,8 +45,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges',
|
||||
'tenant', 'description', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||
'utilization',
|
||||
'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'vlan_count', 'utilization',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
|
||||
validators = []
|
||||
|
||||
@@ -85,7 +85,7 @@ class RIRBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('is_private', 'description'),
|
||||
)
|
||||
nullable_fields = ('is_private', 'description')
|
||||
nullable_fields = ('is_private', 'description', 'comments')
|
||||
|
||||
|
||||
class ASNRangeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
@@ -104,7 +104,7 @@ class ASNRangeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('rir', 'tenant', 'description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class ASNBulkEditForm(PrimaryModelBulkEditForm):
|
||||
@@ -164,7 +164,7 @@ class RoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('weight', 'description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
@@ -379,7 +379,7 @@ class VLANGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||
FieldSet('tenant', name=_('Tenancy')),
|
||||
)
|
||||
nullable_fields = ('description', 'scope')
|
||||
nullable_fields = ('description', 'scope', 'comments')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -84,7 +84,7 @@ class RIRImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ('name', 'slug', 'is_private', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'is_private', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class AggregateImportForm(PrimaryModelImportForm):
|
||||
@@ -124,7 +124,7 @@ class ASNRangeImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ASNRange
|
||||
fields = ('name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class ASNImportForm(PrimaryModelImportForm):
|
||||
@@ -151,7 +151,7 @@ class RoleImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = ('name', 'slug', 'weight', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
@@ -476,7 +476,9 @@ class VLANGroupImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner', 'tags')
|
||||
fields = (
|
||||
'name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner', 'comments', 'tags'
|
||||
)
|
||||
labels = {
|
||||
'scope_id': 'Scope ID',
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class RIRForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = [
|
||||
'name', 'slug', 'is_private', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'is_private', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class ASNRangeForm(TenancyForm, OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = ASNRange
|
||||
fields = [
|
||||
'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'tags'
|
||||
'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ class RoleForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = [
|
||||
'name', 'slug', 'weight', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'weight', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -614,7 +614,8 @@ class VLANGroupForm(TenancyForm, OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'owner', 'comments',
|
||||
'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0084_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asnrange',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rir',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vlangroup',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -31,6 +31,7 @@ class ASNRangeIndex(SearchIndex):
|
||||
fields = (
|
||||
('name', 100),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('rir', 'tenant', 'description')
|
||||
|
||||
@@ -89,6 +90,7 @@ class RIRIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -100,6 +102,7 @@ class RoleIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -156,6 +159,7 @@ class VLANGroupIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('scope_type', 'description')
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ class ASNRangeTable(TenancyColumnsMixin, OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = ASNRange
|
||||
fields = (
|
||||
'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
'pk', 'name', 'slug', 'rir', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description',
|
||||
'comments', 'tags', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'rir', 'start', 'end', 'tenant', 'asn_count', 'description')
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class RIRTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = RIR
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'tags', 'created',
|
||||
'pk', 'id', 'name', 'slug', 'is_private', 'aggregate_count', 'description', 'comments', 'tags', 'created',
|
||||
'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description')
|
||||
@@ -126,8 +126,8 @@ class RoleTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = Role
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'weight', 'tags',
|
||||
'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'name', 'slug', 'prefix_count', 'iprange_count', 'vlan_count', 'description', 'weight',
|
||||
'comments', 'tags', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'prefix_count', 'iprange_count', 'vlan_count', 'description')
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable):
|
||||
model = VLANGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
|
||||
'tenant', 'tenant_group', 'tags', 'created', 'last_updated', 'actions', 'utilization',
|
||||
'tenant', 'tenant_group', 'comments', 'tags', 'created', 'last_updated', 'actions', 'utilization',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'tenant', 'description'
|
||||
|
||||
@@ -91,6 +91,7 @@ class OrganizationalModelBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class NestedGroupModelBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
|
||||
|
||||
@@ -91,6 +91,7 @@ class OrganizationalModelForm(OwnerMixin, NetBoxModelForm):
|
||||
Form for models which inherit from OrganizationalModel.
|
||||
"""
|
||||
slug = SlugField()
|
||||
comments = CommentField()
|
||||
|
||||
|
||||
class NestedGroupModelForm(OwnerMixin, NetBoxModelForm):
|
||||
|
||||
@@ -53,6 +53,7 @@ class OrganizationalModelFilter(NetBoxModelFilter):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -215,6 +215,10 @@ class OrganizationalModel(OwnerMixin, NetBoxModel):
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
verbose_name=_('comments'),
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -285,6 +285,9 @@ class OrganizationalModelTable(NetBoxTable):
|
||||
linkify=True,
|
||||
verbose_name=_('Owner')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
)
|
||||
|
||||
|
||||
class NestedGroupModelTable(NetBoxTable):
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
|
||||
@@ -36,8 +36,8 @@ class ContactRoleSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class ContactRoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class ContactBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -74,7 +74,7 @@ class ContactRoleImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class ContactImportForm(PrimaryModelImportForm):
|
||||
|
||||
@@ -84,7 +84,7 @@ class ContactRoleForm(OrganizationalModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ContactRole
|
||||
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class ContactForm(PrimaryModelForm):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0021_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contactrole',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -37,6 +37,7 @@ class ContactRoleIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class ContactRoleTable(OrganizationalModelTable):
|
||||
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = ContactRole
|
||||
fields = ('pk', 'name', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions')
|
||||
fields = ('pk', 'name', 'description', 'comments', 'slug', 'tags', 'created', 'last_updated', 'actions')
|
||||
default_columns = ('pk', 'name', 'description')
|
||||
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ class ClusterTypeSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'cluster_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'cluster_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count')
|
||||
|
||||
@@ -37,8 +37,8 @@ class ClusterGroupSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'cluster_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'cluster_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count')
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class ClusterTypeBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class ClusterGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
@@ -42,7 +42,7 @@ class ClusterGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet('description'),
|
||||
)
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class ClusterBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -28,14 +28,14 @@ class ClusterTypeImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class ClusterGroupImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class ClusterImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
|
||||
@@ -39,7 +39,7 @@ class ClusterTypeForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = (
|
||||
'name', 'slug', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class ClusterGroupForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = (
|
||||
'name', 'slug', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('virtualization', '0050_virtualmachine_start_on_boot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='clustergroup',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='clustertype',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -20,6 +20,7 @@ class ClusterGroupIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
@@ -31,6 +32,7 @@ class ClusterTypeIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ class ClusterTypeTable(OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = ClusterType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'created', 'last_updated', 'tags', 'actions',
|
||||
'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'comments', 'created', 'last_updated', 'tags',
|
||||
'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||
|
||||
@@ -51,8 +52,8 @@ class ClusterGroupTable(ContactsColumnMixin, OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = ClusterGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'actions',
|
||||
'pk', 'id', 'name', 'slug', 'cluster_count', 'description', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'cluster_count', 'description')
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ class TunnelGroupSerializer(OrganizationalModelSerializer):
|
||||
class Meta:
|
||||
model = TunnelGroup
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'tags', 'custom_fields',
|
||||
'created', 'last_updated', 'tunnel_count',
|
||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'tunnel_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count')
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ __all__ = (
|
||||
|
||||
class TunnelGroupBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
model = TunnelGroup
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('description', 'comments')
|
||||
|
||||
|
||||
class TunnelBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
@@ -28,7 +28,7 @@ class TunnelGroupImportForm(OrganizationalModelImportForm):
|
||||
|
||||
class Meta:
|
||||
model = TunnelGroup
|
||||
fields = ('name', 'slug', 'description', 'owner', 'tags')
|
||||
fields = ('name', 'slug', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class TunnelImportForm(PrimaryModelImportForm):
|
||||
|
||||
@@ -37,7 +37,7 @@ class TunnelGroupForm(OrganizationalModelForm):
|
||||
class Meta:
|
||||
model = TunnelGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'owner', 'tags',
|
||||
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.8 on 2025-12-08 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('vpn', '0010_owner'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tunnelgroup',
|
||||
name='comments',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@@ -21,6 +21,7 @@ class TunnelGroupIndex(SearchIndex):
|
||||
('name', 100),
|
||||
('slug', 110),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('description',)
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ class TunnelGroupTable(ContactsColumnMixin, OrganizationalModelTable):
|
||||
class Meta(OrganizationalModelTable.Meta):
|
||||
model = TunnelGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'tunnel_count', 'description', 'slug', 'contacts', 'tags', 'actions', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'name', 'tunnel_count', 'description', 'comments', 'slug', 'contacts', 'tags', 'actions',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'tunnel_count', 'description')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user