Implement {module} position inheritance for nested module bays #80

Closed
opened 2026-04-05 16:21:31 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @mrmrcoleman on 3/25/2026

Summary

Enables a single ModuleType to produce correctly named components at any nesting depth by resolving {module} in module bay position fields during tree traversal. The user controls the separator through the position field template itself.

Fixes: #19796

Depends on: #21752 ({module} resolution in position field, included in this branch)

Problem

A ModuleType with a single {module} placeholder (e.g., an SFP with interface SFP {module}) cannot be reused at different nesting depths. Installing it at depth 2 requires {module}/{module} in the template name, forcing users to create duplicate ModuleTypes for every possible nesting level.

Solution: Position Inheritance

Instead of introducing a new placeholder token, this PR resolves {module} in each module bay's position field during tree traversal, propagating parent positions through the hierarchy. The user controls the separator by defining it directly in the position field template.

Example

Device bay:      position = "1"
Line card bay:   position = "{module}/2"   --> resolves to "1/2"
SFP template:    name = "SFP {module}"     --> resolves to "SFP 1/2"

Using - instead of /:

Device bay:      position = "1"
Line card bay:   position = "{module}-2"   --> resolves to "1-2"
SFP template:    name = "eth{module}"      --> resolves to "eth1-2"

Why not {module_path}?

The previous approach (PR #21436) introduced a new {module_path} token with a hardcoded / separator. This approach is simpler and more flexible:

  • No new tokens or constants
  • No database migration
  • Separator is user-controlled, not hardcoded (addresses feedback from #21667)
  • Backwards compatible — inheritance only activates when positions contain {module}

Credit to @rhartmann for suggesting this approach in the #21436 discussion.

Changes

File Change
dcim/models/device_component_templates.py Add _get_inherited_positions() to resolve positions during tree traversal; update _resolve_module_placeholder() with single-token logic (resolves to leaf's inherited position); add resolve_position() for position field resolution (#20467); consolidate into _resolve_placeholders() with {vc_position} support
dcim/forms/common.py Add _get_inherited_positions() static method; extract _validate_module_tokens() private method; update clean() to use inherited positions
dcim/tests/test_models.py Tests for position inheritance at depth 2 and 3, custom separators, multi-token backwards compatibility, position field resolution
docs/models/dcim/moduletype.md Document position inheritance behavior

Testing

All 1103 dcim tests pass (model, form, API). Added 5 new tests:

  • Position field resolution (#20467): {module}-1 resolves to 1-1
  • Inheritance depth 2: position {module}/2 resolves to 1/2, interface SFP {module} resolves to SFP 1/2
  • Inheritance depth 3: chained inheritance through 3 levels
  • Custom separator: . separator produces eth1.1
  • Multi-token backwards compat: Gi{module}/{module} at matching depth still resolves level-by-level

Design decisions

  • No object mutation during form validation_get_inherited_positions() builds a separate list of resolved position strings without modifying module bay model instances
  • Single token = leaf position: a single {module} resolves to the leaf bay's inherited position regardless of depth
  • Multi token = level-by-level: multiple {module} tokens must match tree depth exactly (unchanged behavior)
  • {vc_position} integration: position inheritance works alongside the {vc_position} template variable
*Originally created by @mrmrcoleman on 3/25/2026* ## Summary Enables a single ModuleType to produce correctly named components at any nesting depth by resolving `{module}` in module bay position fields during tree traversal. The user controls the separator through the position field template itself. **Fixes:** #19796 **Depends on:** #21752 (`{module}` resolution in position field, included in this branch) ## Problem A ModuleType with a single `{module}` placeholder (e.g., an SFP with interface `SFP {module}`) cannot be reused at different nesting depths. Installing it at depth 2 requires `{module}/{module}` in the template name, forcing users to create duplicate ModuleTypes for every possible nesting level. ## Solution: Position Inheritance Instead of introducing a new placeholder token, this PR resolves `{module}` in each module bay's **position field** during tree traversal, propagating parent positions through the hierarchy. The user controls the separator by defining it directly in the position field template. ### Example ``` Device bay: position = "1" Line card bay: position = "{module}/2" --> resolves to "1/2" SFP template: name = "SFP {module}" --> resolves to "SFP 1/2" ``` Using `-` instead of `/`: ``` Device bay: position = "1" Line card bay: position = "{module}-2" --> resolves to "1-2" SFP template: name = "eth{module}" --> resolves to "eth1-2" ``` ### Why not `{module_path}`? The previous approach (PR #21436) introduced a new `{module_path}` token with a hardcoded `/` separator. This approach is simpler and more flexible: - No new tokens or constants - No database migration - Separator is user-controlled, not hardcoded (addresses feedback from #21667) - Backwards compatible — inheritance only activates when positions contain `{module}` Credit to @rhartmann for suggesting this approach in the #21436 discussion. ## Changes | File | Change | |------|--------| | `dcim/models/device_component_templates.py` | Add `_get_inherited_positions()` to resolve positions during tree traversal; update `_resolve_module_placeholder()` with single-token logic (resolves to leaf's inherited position); add `resolve_position()` for position field resolution (#20467); consolidate into `_resolve_placeholders()` with `{vc_position}` support | | `dcim/forms/common.py` | Add `_get_inherited_positions()` static method; extract `_validate_module_tokens()` private method; update `clean()` to use inherited positions | | `dcim/tests/test_models.py` | Tests for position inheritance at depth 2 and 3, custom separators, multi-token backwards compatibility, position field resolution | | `docs/models/dcim/moduletype.md` | Document position inheritance behavior | ## Testing All 1103 dcim tests pass (model, form, API). Added 5 new tests: - **Position field resolution** (#20467): `{module}-1` resolves to `1-1` - **Inheritance depth 2**: position `{module}/2` resolves to `1/2`, interface `SFP {module}` resolves to `SFP 1/2` - **Inheritance depth 3**: chained inheritance through 3 levels - **Custom separator**: `.` separator produces `eth1.1` - **Multi-token backwards compat**: `Gi{module}/{module}` at matching depth still resolves level-by-level ## Design decisions - **No object mutation during form validation** — `_get_inherited_positions()` builds a separate list of resolved position strings without modifying module bay model instances - **Single token = leaf position**: a single `{module}` resolves to the leaf bay's inherited position regardless of depth - **Multi token = level-by-level**: multiple `{module}` tokens must match tree depth exactly (unchanged behavior) - **`{vc_position}` integration**: position inheritance works alongside the `{vc_position}` template variable
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/netbox#80