-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Fix {module} placeholder resolution + add {module_path} for nested modules #21073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature
Are you sure you want to change the base?
Conversation
sigprof
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issues to discuss:
- Should we redefine the meaning of
{module}(so that a single use of{module}means the whole path, but multiple uses of{module}in the same attribute each expand to a single level ofposition), or define a new placeholder like{module_path}which must be used only once in the string (and then keep the existing restrictions for{module})? The latter may be better for backwards compatibility, but some people would need to replace{module}with{module_path}in their module types when they encounter devices with multilevel module hierarchy. - How should the separators in the
{module_path}expansion be handled? In general, the separator may need to be specified by the device (and in some cases multiple kinds of separators may be needed, like<UNIT>/<SLOT>/<PORT>:<CHANNEL>, although in this case the:<CHANNEL>part may need to be supplied by the transceiver module). - More generally, should the
positionattribute for a module bay contain only the parent-relative position, or the full path from the device? The current code assumes the first interpretation (therefore expanding{module_path}requires concatenating thepositionvalues along the path and adding separators); however, if we choose the second interpretation (which would also require fixing #20467, so that thepositionattribute for the module bay on a module could reference the position of the parent module bay), we could avoid at least some issues with vendor-specific separators: the modules which correspond to interface cards (which are inevitably vendor-specific) could include all separators in theirpositionvalues, so that the module bays which accept generic transceiver modules could have theirpositionvalues always equal to the interface names.
5503e64 to
95caffb
Compare
- 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.
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.
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
|
The suggested fixes for #20474/#19796 and #20467 are somewhat conflicting:
E.g., look at the intended usage in #20467:
Possible options here:
In the first case modules which can be used at any depth would have the In the second case there would be two ways to build the hierarchy:
One problem here is that the choice between these two ways to build the hierarchy needs to be made at the start — if you already have some module types set up for one way, you can't reuse those types without changes for the other way. In the third case only the way 2 above would be possible if you want to build a multilevel hiearchy with generic modules that accept any number of nested levels. |
95caffb to
1c6adc4
Compare
…only
Per sigprof's feedback, this implements two distinct placeholders:
- {module_path}: Always expands to full /-separated path (e.g., 1/2/3)
Use case: Generic modules like SFPs that work at any depth
- {module} (single): Expands to parent bay position only
Use case: Building custom paths via position field with user-controlled separators
- {module}/{module}: Level-by-level substitution (unchanged for backwards compat)
This design allows two ways to build module hierarchies:
1. Use {module_path} for automatic path joining (hardcodes / separator)
2. Use position field with {module} for custom separators
Fixes #20474, #20467, #19796
|
@sigprof Your input is appreciated. Please take a look at the recent changes and updated PR description |
Constants should only contain constant values, not functions with logic. The helper function now lives in dcim/utils.py alongside other utilities like update_interface_bridges and create_port_mappings.
1. Add validation to reject mixing {module} and {module_path} in same attribute
2. Refactor resolve_position() to match resolve_name()/resolve_label() pattern
- Moved to ModuleBayTemplate where it can access self.position directly
- No longer takes position as argument
3. Added test for mixed placeholder validation
arthanson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add documentation for {module_path} to docs/models/dcim/moduletype.md under ## Automatic Component Renaming
Per arthanson's review request, updated docs/models/dcim/moduletype.md
to document:
- {module} placeholder behavior (single vs multiple use)
- {module_path} placeholder for full path expansion
- Position field resolution for nested module bays
5550f13 to
b6548d9
Compare
| def test_mixed_module_and_module_path_fails_validation(self): | ||
| """ | ||
| Test that mixing {module} and {module_path} in the same template attribute | ||
| fails validation. Per sigprof's feedback, these cannot be combined. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can probably get rid of the "Per sigprof's feedback..."
| site = Site.objects.first() | ||
| device_role = DeviceRole.objects.first() | ||
|
|
||
| device_type = DeviceType.objects.create( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it make sense to move device_type creation into setup, it seems to be used in almost every test.
| # Single {module} should give PARENT position only: Y (not X/Y) | ||
| self.assertEqual(interface.name, 'Port Y') | ||
|
|
||
| def test_sigprof_nested_position_no_duplication(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the tests are good but a few things to check:
- There looks like some duplication, like checking for depth 1, then 2, then 3, I think just 1 and 3 would work potentially. Also test_nested_module_bay_label_resolution looks like it could be folded into one of the new tests.
- I'd re-order these to make them easier to check in the future - put all the {module} tests first, then the {module_path} test next, then the combined ones after. keep the simple 'test single {module}' first then test multi module after it, it is sort of like this at start but then the tests get jumbled together. Move test_single_module_token down with the other {module} tests.
| ## Automatic Component Renaming | ||
|
|
||
| When adding component templates to a module type, the string `{module}` can be used to reference the `position` field of the module bay into which an instance of the module type is being installed. | ||
| When adding component templates to a module type, placeholders can be used to dynamically incorporate the module bay's `position` field into component names. Two placeholders are available: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit confusing - can these both be used in both the module_name and position fields? "Position Field Resolution" says only {module} can be there, but resolve_position function makes it sound like {module_name} can be their also? Can multiple {module} be in the position field?
If not the validators should be updated for position field and tests for it as well.
|
|
||
| for module_bay in module_bays: | ||
| resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1) | ||
| # Validate {module_path} - can only appear once |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe move the token validator to a separate private function to make clearer.
| 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)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this has common code with resolve_label, can just make a private function - something like:
def _resolve_placeholders(self, text, module):
"""Resolve {module} and {module_path} placeholders in text (module must be set)."""
positions = [m.module_bay.position for m in self._get_module_tree(module)]
return resolve_module_placeholders(text, positions)
def resolve_name(self, module):
if not module:
return self.name
return self._resolve_placeholders(self.name, module)
def resolve_label(self, module):
if not module:
return self.label
return self._resolve_placeholders(self.label, module)
| Fixes Issue #20467. | ||
| """ | ||
| if module: | ||
| positions = [m.module_bay.position for m in self._get_module_tree(module)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually this seems duplicated in the my suggestion above - can the position and resolve_module_placeholders just be made into a common function. Currently duplicated three times.
Summary
Fixes the
{module}placeholder resolution for nested module bays and introduces a new{module_path}placeholder for automatic full-path expansion.Fixes: #20474, #20467, #19796
Problem
The current implementation requires an exact match between the number of
{module}tokens in template names and the depth of the module bay tree. This forces users to create duplicate ModuleTypes for each nesting level, breaking the "pluggable transceivers" use case where a single SFP/QSFP module should install at any depth.Additionally, the
{module}placeholder was not resolved in thepositionfield of module bays, leaving literal{module}strings in positions when installing nested modules.Solution
This PR introduces two distinct placeholders:
{module_path}(NEW)/{module}(UPDATED BEHAVIOR)Why Two Placeholders?
This design supports two complementary workflows:
{module_path}in component names for automatic path joining{module}in position field to build paths likeeth{module}-{position}where users control the separator in the position field itselfValidation Rules
{module_path}allowed per template attribute{module}and{module_path}in the same attribute{module}tokens must match tree depth exactly (existing behavior)Testing
{module_path}at depths 1, 2, and 3{module}parent-only behavior{module}and{module_path}Why This Targets
featureBranchThis PR targets
featurebecause it introduces new functionality ({module_path}placeholder). However, all changes are backwards compatible:{module}usage now gives parent position (previously gave full path for depth-1 only){module}behavior is unchangedTo target
mainfor 4.5.x: The behavioral change for single{module}(parent-only vs. full-path) is technically a change, but since the previous behavior was problematic for nested modules anyway, this could be considered a bug fix. Maintainers can decide if this warrants backporting.Changes
dcim/constants.py: AddedMODULE_PATH_TOKEN,resolve_module_placeholders()helperdcim/forms/common.py: Updated validation for both placeholder typesdcim/models/device_component_templates.py: Uses centralized helper for name/label/position resolutiondcim/tests/test_models.py: Comprehensive test coverage for all scenarios