Compare commits

...

5 Commits

Author SHA1 Message Date
Mark Coleman
95caffb164 Refactor: centralize module token substitution logic
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
Per sigprof's review feedback, extract the duplicated token substitution
logic into a single resolve_module_token() helper in constants.py.

This addresses two review comments:
1. Duplication between ModuleCommonForm.clean() and resolve_name()
2. Duplication between resolve_name() and resolve_label()

Benefits:
- Single source of truth for substitution logic
- MODULE_TOKEN_SEPARATOR constant for future configurability
- Cleaner, more maintainable code (-7 net lines)
- Easier to modify separator handling in one place
2026-01-19 16:14:20 +01:00
Mark Coleman
233e623783 Address sigprof review: stricter token validation
Per sigprof's feedback, the previous validation (depth >= token_count)
allowed a questionable case where token_count > 1 but < depth, which
would lose position information for some levels.

New validation: token_count must be either 1 (full path expansion) or
exactly match the tree depth (level-by-level substitution).

Updated test T2 to verify this mismatched case is now rejected.
2026-01-19 16:03:12 +01:00
Mark Coleman
8c4ba36319 Fix PEP8: remove trailing whitespace from blank lines 2026-01-19 15:59:18 +01:00
Mark Coleman
a1c3eb2b1d Add position field resolution for module bays (fixes #20467)
- Add resolve_position() method to ModularComponentTemplateModel
- Update ModuleBayTemplate.instantiate() to resolve {module} in position field
- Add test_module_bay_position_resolves_placeholder test

This completes the fix for nested module placeholder issues by ensuring
the position field also resolves {module} placeholders, which is required
for building correct full paths in 3+ level hierarchies.
2026-01-19 15:54:49 +01:00
Mark Coleman
82f6892d24 Fix nested module bay placeholder: single {module} resolves to full path (e.g., 1/1) 2026-01-05 22:08:18 +01:00
4 changed files with 866 additions and 23 deletions

View File

@@ -79,6 +79,41 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
#
MODULE_TOKEN = '{module}'
MODULE_TOKEN_SEPARATOR = '/'
def resolve_module_token(text, positions):
"""
Substitute {module} tokens in text with position values.
Args:
text: String potentially containing {module} tokens
positions: List of position strings from the module tree (root to leaf)
Returns:
Text with {module} tokens replaced according to these rules:
- Single token: replaced with full path (positions joined by MODULE_TOKEN_SEPARATOR)
- Multiple tokens: replaced level-by-level (first token gets first position, etc.)
This centralizes the substitution logic used by both ModuleCommonForm.clean()
and ModularComponentTemplateModel.resolve_*() methods.
"""
if not text or MODULE_TOKEN not in text:
return text
token_count = text.count(MODULE_TOKEN)
if token_count == 1:
# Single token: substitute with full path (e.g., "1/1" for depth 2)
full_path = MODULE_TOKEN_SEPARATOR.join(positions)
return text.replace(MODULE_TOKEN, full_path, 1)
else:
# Multiple tokens: substitute level-by-level (existing behavior)
result = text
for pos in positions:
result = result.replace(MODULE_TOKEN, pos, 1)
return result
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
app_label='dcim',

View File

@@ -126,18 +126,22 @@ class ModuleCommonForm(forms.Form):
_("Cannot install module with placeholder values in a module bay with no position defined.")
)
if len(module_bays) != template.name.count(MODULE_TOKEN):
token_count = template.name.count(MODULE_TOKEN)
# A single token which gets expanded to the full path is always
# allowed; otherwise the number of tokens needs to match the path length.
if token_count != 1 and token_count != len(module_bays):
raise forms.ValidationError(
_(
"Cannot install module with placeholder values in a module bay tree {level} in tree "
"but {tokens} placeholders given."
).format(
level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
level=len(module_bays), tokens=token_count
)
)
for module_bay in module_bays:
resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1)
# Use centralized helper for token substitution
positions = [mb.position for mb in module_bays]
resolved_name = resolve_module_token(resolved_name, positions)
existing_item = installed_components.get(resolved_name)

View File

@@ -170,29 +170,33 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
return modules
def resolve_name(self, module):
if MODULE_TOKEN not in self.name:
return self.name
"""Resolve {module} placeholder(s) in component name."""
if module:
modules = self._get_module_tree(module)
name = self.name
for module in modules:
name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
return name
positions = [m.module_bay.position for m in self._get_module_tree(module)]
return resolve_module_token(self.name, positions)
return self.name
def resolve_label(self, module):
if MODULE_TOKEN not in self.label:
return self.label
"""Resolve {module} placeholder(s) in component label."""
if module:
modules = self._get_module_tree(module)
label = self.label
for module in modules:
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
return label
positions = [m.module_bay.position for m in self._get_module_tree(module)]
return resolve_module_token(self.label, positions)
return self.label
def resolve_position(self, position, module):
"""
Resolve {module} placeholder in position field.
This is used by ModuleBayTemplate to resolve positions like "{module}/1"
to actual values like "A/1" when the parent module is installed in bay "A".
Fixes Issue #20467.
"""
if module:
positions = [m.module_bay.position for m in self._get_module_tree(module)]
return resolve_module_token(position, positions)
return position
class ConsolePortTemplate(ModularComponentTemplateModel):
"""
@@ -722,10 +726,11 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
verbose_name_plural = _('module bay templates')
def instantiate(self, **kwargs):
module = kwargs.get('module')
return self.component_model(
name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')),
position=self.position,
name=self.resolve_name(module),
label=self.resolve_label(module),
position=self.resolve_position(self.position, module),
**kwargs
)
instantiate.do_not_call_in_templates = True

View File

@@ -848,6 +848,805 @@ class ModuleBayTestCase(TestCase):
nested_bay = module.modulebays.get(name='SFP A-21')
self.assertEqual(nested_bay.label, 'A-21')
def test_nested_module_single_placeholder_full_path(self):
"""
Test that installing a module at depth=2 with a single {module} placeholder
in the interface template name resolves to the full path (e.g., "1/1").
Regression test for transceiver modeling use case.
"""
manufacturer = Manufacturer.objects.first()
site = Site.objects.first()
device_role = DeviceRole.objects.first()
# Create device type with module bay template
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Chassis Device',
slug='chassis-device'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Line Card Bay 1',
position='1'
)
# Create line card module type with nested module bay
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='SFP Bay {module}/1',
label='SFP {module}/1',
position='1'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='SFP Bay {module}/2',
label='SFP {module}/2',
position='2'
)
# Create SFP module type with interface using single {module} placeholder
sfp_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='SFP Transceiver'
)
InterfaceTemplate.objects.create(
module_type=sfp_type,
name='SFP {module}',
label='{module}',
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
)
# Create device
device = Device.objects.create(
name='Test Chassis',
device_type=device_type,
role=device_role,
site=site
)
# Install line card in bay 1
line_card_bay = device.modulebays.get(name='Line Card Bay 1')
line_card = Module.objects.create(
device=device,
module_bay=line_card_bay,
module_type=line_card_type
)
# Install SFP in nested bay 1 (depth=2)
sfp_bay_1 = line_card.modulebays.get(name='SFP Bay 1/1')
sfp_module_1 = Module.objects.create(
device=device,
module_bay=sfp_bay_1,
module_type=sfp_type
)
# Verify interface name resolves to full path "1/1"
interface_1 = sfp_module_1.interfaces.first()
self.assertEqual(interface_1.name, 'SFP 1/1')
self.assertEqual(interface_1.label, '1/1')
# Install second SFP in nested bay 2 (depth=2) - verifies uniqueness
sfp_bay_2 = line_card.modulebays.get(name='SFP Bay 1/2')
sfp_module_2 = Module.objects.create(
device=device,
module_bay=sfp_bay_2,
module_type=sfp_type
)
# Verify second interface name resolves to full path "1/2"
interface_2 = sfp_module_2.interfaces.first()
self.assertEqual(interface_2.name, 'SFP 1/2')
self.assertEqual(interface_2.label, '1/2')
def test_module_bay_position_resolves_placeholder(self):
"""
Test that the position field of instantiated module bays resolves {module} placeholder.
Issue #20467: When a module type has module bay templates with position="{module}/1",
the instantiated module bay should have position="A/1" (not literal "{module}/1").
This test should:
- FAIL on main branch (bug present: position contains "{module}")
- PASS after fix (position is resolved to actual value)
"""
manufacturer = Manufacturer.objects.first()
site = Site.objects.first()
device_role = DeviceRole.objects.first()
# Create device type with module bay at position 'A'
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Position Test Chassis',
slug='position-test-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay A',
position='A'
)
# Create module type with nested bays using {module} in POSITION field
extension_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='Position Test Extension'
)
ModuleBayTemplate.objects.create(
module_type=extension_type,
name='Sub Bay {module}-1',
label='{module}-1',
position='{module}/1' # This should resolve to "A/1"
)
ModuleBayTemplate.objects.create(
module_type=extension_type,
name='Sub Bay {module}-2',
label='{module}-2',
position='{module}/2' # This should resolve to "A/2"
)
# Create device
device = Device.objects.create(
name='Position Test Device',
device_type=device_type,
role=device_role,
site=site
)
# Install extension module in Bay A
parent_bay = device.modulebays.get(name='Bay A')
module = Module.objects.create(
device=device,
module_bay=parent_bay,
module_type=extension_type
)
# Verify the nested bays have resolved names (this already works)
nested_bay_1 = module.modulebays.get(name='Sub Bay A-1')
nested_bay_2 = module.modulebays.get(name='Sub Bay A-2')
# Verify labels are resolved (this already works)
self.assertEqual(nested_bay_1.label, 'A-1')
self.assertEqual(nested_bay_2.label, 'A-2')
# Verify POSITION field is resolved (Issue #20467 - this currently fails)
self.assertEqual(nested_bay_1.position, 'A/1')
self.assertEqual(nested_bay_2.position, 'A/2')
# Also verify no {module} literal remains
self.assertNotIn('{module}', nested_bay_1.position)
self.assertNotIn('{module}', nested_bay_2.position)
def test_single_placeholder_direct_install_depth_1(self):
"""
Test that installing a module directly at depth=1 with a single {module}
placeholder still resolves correctly (just the position, not a path).
"""
manufacturer = Manufacturer.objects.first()
site = Site.objects.first()
device_role = DeviceRole.objects.first()
# Create device type with module bay template
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='Simple Chassis',
slug='simple-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='SFP Bay 1',
position='1'
)
# Create SFP module type with interface using single {module} placeholder
sfp_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='Direct SFP'
)
InterfaceTemplate.objects.create(
module_type=sfp_type,
name='SFP {module}',
label='{module}',
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
)
# Create device
device = Device.objects.create(
name='Test Simple Chassis',
device_type=device_type,
role=device_role,
site=site
)
# Install SFP directly in bay 1 (depth=1)
sfp_bay = device.modulebays.get(name='SFP Bay 1')
sfp_module = Module.objects.create(
device=device,
module_bay=sfp_bay,
module_type=sfp_type
)
# Verify interface name resolves to just "1"
interface = sfp_module.interfaces.first()
self.assertEqual(interface.name, 'SFP 1')
self.assertEqual(interface.label, '1')
def test_multi_token_level_by_level_depth_2(self):
"""
T1: Multi-token behavior remains unchanged at depth=2.
Ensure legacy {module}/{module} still resolves level-by-level.
"""
site = Site.objects.create(name='T1 Site', slug='t1-site')
manufacturer = Manufacturer.objects.create(name='T1 Manufacturer', slug='t1-manufacturer')
device_role = DeviceRole.objects.create(name='T1 Role', slug='t1-role')
# Create device type with module bay
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T1 Chassis',
slug='t1-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 1',
position='1'
)
# Create line card module type with nested bay
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T1 Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='Nested Bay 2',
position='2'
)
# Create SFP module type with 2-token interface template
sfp_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T1 SFP'
)
InterfaceTemplate.objects.create(
module_type=sfp_type,
name='SFP {module}/{module}',
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
)
# Create device and install modules
device = Device.objects.create(
name='T1 Device',
device_type=device_type,
role=device_role,
site=site
)
# Install line card at position 1
line_card_bay = device.modulebays.get(name='Bay 1')
line_card = Module.objects.create(
device=device,
module_bay=line_card_bay,
module_type=line_card_type
)
# Install SFP at nested bay (position 2)
sfp_bay = line_card.modulebays.get(name='Nested Bay 2')
sfp_module = Module.objects.create(
device=device,
module_bay=sfp_bay,
module_type=sfp_type
)
# Verify level-by-level substitution: 1/2 (not 1/2/1/2)
interface = sfp_module.interfaces.first()
self.assertEqual(interface.name, 'SFP 1/2')
def test_mismatched_multi_token_fails_validation(self):
"""
T2: Multi-token with mismatched depth fails validation (depth=3, tokens=2).
Per sigprof's feedback: allowing this would lose position info for level 3.
Only single-token (full path) or exact-match multi-token should be allowed.
"""
from dcim.forms import ModuleForm
site = Site.objects.create(name='T2 Site', slug='t2-site')
manufacturer = Manufacturer.objects.create(name='T2 Manufacturer', slug='t2-manufacturer')
device_role = DeviceRole.objects.create(name='T2 Role', slug='t2-role')
# Create device type with module bay
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T2 Chassis',
slug='t2-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 1',
position='1'
)
# Create level 2 module type with nested bay
level2_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T2 Level2'
)
ModuleBayTemplate.objects.create(
module_type=level2_type,
name='Level2 Bay',
position='1'
)
# Create level 3 module type with nested bay
level3_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T2 Level3'
)
ModuleBayTemplate.objects.create(
module_type=level3_type,
name='Level3 Bay',
position='1'
)
# Create leaf module type with 2-token interface template (mismatched for depth 3)
leaf_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T2 Leaf'
)
InterfaceTemplate.objects.create(
module_type=leaf_type,
name='SFP {module}/{module}',
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
)
# Create device and install first 2 levels of modules
device = Device.objects.create(
name='T2 Device',
device_type=device_type,
role=device_role,
site=site
)
# Level 1
bay1 = device.modulebays.get(name='Bay 1')
module1 = Module.objects.create(
device=device,
module_bay=bay1,
module_type=level2_type
)
# Level 2
bay2 = module1.modulebays.get(name='Level2 Bay')
module2 = Module.objects.create(
device=device,
module_bay=bay2,
module_type=level3_type
)
# Attempt to install leaf module at depth=3 with 2 tokens - should fail
bay3 = module2.modulebays.get(name='Level3 Bay')
form = ModuleForm(data={
'device': device.pk,
'module_bay': bay3.pk,
'module_type': leaf_type.pk,
'status': 'active',
'replicate_components': True,
'adopt_components': False,
})
# Validation should fail: 2 tokens != 1 and 2 tokens != 3 depth
self.assertFalse(form.is_valid())
self.assertIn('2', str(form.errors))
self.assertIn('3', str(form.errors))
def test_too_many_tokens_fails_validation(self):
"""
T3: Too-many-tokens still fails (depth=2, tokens=3).
Confirms the validation prevents impossible substitution.
"""
from dcim.forms import ModuleForm
site = Site.objects.create(name='T3 Site', slug='t3-site')
manufacturer = Manufacturer.objects.create(name='T3 Manufacturer', slug='t3-manufacturer')
device_role = DeviceRole.objects.create(name='T3 Role', slug='t3-role')
# Create device type with module bay
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T3 Chassis',
slug='t3-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 1',
position='1'
)
# Create line card module type with nested bay
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T3 Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='Nested Bay',
position='1'
)
# Create leaf module type with 3-token interface template (too many!)
leaf_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T3 Leaf'
)
InterfaceTemplate.objects.create(
module_type=leaf_type,
name='{module}/{module}/{module}',
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
)
# Create device and install line card
device = Device.objects.create(
name='T3 Device',
device_type=device_type,
role=device_role,
site=site
)
bay1 = device.modulebays.get(name='Bay 1')
line_card = Module.objects.create(
device=device,
module_bay=bay1,
module_type=line_card_type
)
# Attempt to install leaf module at depth=2 with 3 tokens - should fail
nested_bay = line_card.modulebays.get(name='Nested Bay')
form = ModuleForm(data={
'device': device.pk,
'module_bay': nested_bay.pk,
'module_type': leaf_type.pk,
'status': 'active',
'replicate_components': True,
'adopt_components': False,
})
self.assertFalse(form.is_valid())
# Check the error message mentions the mismatch
self.assertIn('2', str(form.errors))
self.assertIn('3', str(form.errors))
def test_label_substitution_matches_name_depth_2(self):
"""
T4: Label substitution works the same way as name (depth=2 single-token).
"""
site = Site.objects.create(name='T4 Site', slug='t4-site')
manufacturer = Manufacturer.objects.create(name='T4 Manufacturer', slug='t4-manufacturer')
device_role = DeviceRole.objects.create(name='T4 Role', slug='t4-role')
# Create device type with module bay
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T4 Chassis',
slug='t4-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 1',
position='1'
)
# Create line card module type with nested bay at position 2
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T4 Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='Nested Bay',
position='2'
)
# Create leaf module type with single-token name AND label
leaf_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T4 Leaf'
)
InterfaceTemplate.objects.create(
module_type=leaf_type,
name='SFP {module}',
label='LBL {module}',
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
)
# Create device and install modules
device = Device.objects.create(
name='T4 Device',
device_type=device_type,
role=device_role,
site=site
)
bay1 = device.modulebays.get(name='Bay 1')
line_card = Module.objects.create(
device=device,
module_bay=bay1,
module_type=line_card_type
)
nested_bay = line_card.modulebays.get(name='Nested Bay')
leaf_module = Module.objects.create(
device=device,
module_bay=nested_bay,
module_type=leaf_type
)
# Verify both name and label resolve to full path
interface = leaf_module.interfaces.first()
self.assertEqual(interface.name, 'SFP 1/2')
self.assertEqual(interface.label, 'LBL 1/2')
def test_non_interface_component_template_substitution(self):
"""
T5: Non-interface modular component templates (ConsolePortTemplate).
Ensures the fix is general to all ModularComponentTemplateModel subclasses.
"""
site = Site.objects.create(name='T5 Site', slug='t5-site')
manufacturer = Manufacturer.objects.create(name='T5 Manufacturer', slug='t5-manufacturer')
device_role = DeviceRole.objects.create(name='T5 Role', slug='t5-role')
# Create device type with module bay
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T5 Chassis',
slug='t5-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 1',
position='1'
)
# Create line card module type with nested bay at position 2
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T5 Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='Nested Bay',
position='2'
)
# Create leaf module type with ConsolePortTemplate using single token
leaf_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T5 Leaf'
)
ConsolePortTemplate.objects.create(
module_type=leaf_type,
name='Console {module}',
label='{module}'
)
# Create device and install modules
device = Device.objects.create(
name='T5 Device',
device_type=device_type,
role=device_role,
site=site
)
bay1 = device.modulebays.get(name='Bay 1')
line_card = Module.objects.create(
device=device,
module_bay=bay1,
module_type=line_card_type
)
nested_bay = line_card.modulebays.get(name='Nested Bay')
leaf_module = Module.objects.create(
device=device,
module_bay=nested_bay,
module_type=leaf_type
)
# Verify ConsolePort resolves with full path
console_port = leaf_module.consoleports.first()
self.assertEqual(console_port.name, 'Console 1/2')
self.assertEqual(console_port.label, '1/2')
def test_positions_with_slashes_join_correctly(self):
"""
T6: Positions that already contain slashes don't break joining (depth=2, single token).
Some platforms use positions like 0/1 (PIC/port style) even before nesting.
"""
site = Site.objects.create(name='T6 Site', slug='t6-site')
manufacturer = Manufacturer.objects.create(name='T6 Manufacturer', slug='t6-manufacturer')
device_role = DeviceRole.objects.create(name='T6 Role', slug='t6-role')
# Create device type with module bay using slash in position
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T6 Chassis',
slug='t6-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='PIC Bay',
position='0/1' # Position already contains slash
)
# Create line card module type with nested bay at position 2
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T6 Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='Nested Bay',
position='2'
)
# Create leaf module type with single-token interface template
leaf_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T6 Leaf'
)
InterfaceTemplate.objects.create(
module_type=leaf_type,
name='Gi{module}',
type=InterfaceTypeChoices.TYPE_1GE_FIXED
)
# Create device and install modules
device = Device.objects.create(
name='T6 Device',
device_type=device_type,
role=device_role,
site=site
)
bay1 = device.modulebays.get(name='PIC Bay')
line_card = Module.objects.create(
device=device,
module_bay=bay1,
module_type=line_card_type
)
nested_bay = line_card.modulebays.get(name='Nested Bay')
leaf_module = Module.objects.create(
device=device,
module_bay=nested_bay,
module_type=leaf_type
)
# Verify: 0/1 + 2 = 0/1/2
interface = leaf_module.interfaces.first()
self.assertEqual(interface.name, 'Gi0/1/2')
def test_depth_1_single_token_no_extra_slashes(self):
"""
T7: Ensure depth=1 single-token still resolves to the position, not an unnecessary "path join".
"""
site = Site.objects.create(name='T7 Site', slug='t7-site')
manufacturer = Manufacturer.objects.create(name='T7 Manufacturer', slug='t7-manufacturer')
device_role = DeviceRole.objects.create(name='T7 Role', slug='t7-role')
# Create device type with module bay at position 7
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T7 Chassis',
slug='t7-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 7',
position='7'
)
# Create module type with single-token template
module_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T7 Module'
)
InterfaceTemplate.objects.create(
module_type=module_type,
name='{module}',
type=InterfaceTypeChoices.TYPE_1GE_FIXED
)
# Create device and install module directly at depth=1
device = Device.objects.create(
name='T7 Device',
device_type=device_type,
role=device_role,
site=site
)
bay = device.modulebays.get(name='Bay 7')
module = Module.objects.create(
device=device,
module_bay=bay,
module_type=module_type
)
# Verify: just "7", not "7/" or similar
interface = module.interfaces.first()
self.assertEqual(interface.name, '7')
def test_multi_occurrence_tokens_level_by_level(self):
"""
T8: Multiple occurrences of {module} in a single template (token_count > 1) still level-by-level.
Ensure the token_count logic and replacement loop behaves with duplicated patterns.
"""
site = Site.objects.create(name='T8 Site', slug='t8-site')
manufacturer = Manufacturer.objects.create(name='T8 Manufacturer', slug='t8-manufacturer')
device_role = DeviceRole.objects.create(name='T8 Role', slug='t8-role')
# Create device type with module bay
device_type = DeviceType.objects.create(
manufacturer=manufacturer,
model='T8 Chassis',
slug='t8-chassis'
)
ModuleBayTemplate.objects.create(
device_type=device_type,
name='Bay 1',
position='1'
)
# Create line card module type with nested bay at position 2
line_card_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T8 Line Card'
)
ModuleBayTemplate.objects.create(
module_type=line_card_type,
name='Nested Bay',
position='2'
)
# Create leaf module type with 2-token template (non-slash separator)
leaf_type = ModuleType.objects.create(
manufacturer=manufacturer,
model='T8 Leaf'
)
InterfaceTemplate.objects.create(
module_type=leaf_type,
name='X{module}-Y{module}',
type=InterfaceTypeChoices.TYPE_1GE_FIXED
)
# Create device and install modules
device = Device.objects.create(
name='T8 Device',
device_type=device_type,
role=device_role,
site=site
)
bay1 = device.modulebays.get(name='Bay 1')
line_card = Module.objects.create(
device=device,
module_bay=bay1,
module_type=line_card_type
)
nested_bay = line_card.modulebays.get(name='Nested Bay')
leaf_module = Module.objects.create(
device=device,
module_bay=nested_bay,
module_type=leaf_type
)
# Verify: X1-Y2 (level-by-level, not full-path stuffed into first)
interface = leaf_module.interfaces.first()
self.assertEqual(interface.name, 'X1-Y2')
class CableTestCase(TestCase):