mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-20 18:48:45 -06:00
Compare commits
9 Commits
20490-rest
...
fix_module
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6548d941b | ||
|
|
898fe8b3d8 | ||
|
|
3680b0ccd4 | ||
|
|
702b1f8210 | ||
|
|
1c6adc40b3 | ||
|
|
bcd3851f4e | ||
|
|
850bfba9e4 | ||
|
|
1df6eee467 | ||
|
|
e613b55ada |
@@ -16,9 +16,33 @@ Note that device bays and module bays may _not_ be added to modules.
|
|||||||
|
|
||||||
## Automatic Component Renaming
|
## 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:
|
||||||
|
|
||||||
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
|
### `{module}` Placeholder
|
||||||
|
|
||||||
|
The `{module}` placeholder references the position of the parent module bay:
|
||||||
|
|
||||||
|
* **Single use**: Expands to the immediate parent's position only
|
||||||
|
* **Multiple uses**: Each `{module}` token is replaced level-by-level (the number of tokens must match the nesting depth)
|
||||||
|
|
||||||
|
For example, a module type with interface templates named `Gi{module}/0/[1-48]`, when installed in a module bay with position "3", will create interfaces named `Gi3/0/[1-48]`.
|
||||||
|
|
||||||
|
### `{module_path}` Placeholder
|
||||||
|
|
||||||
|
The `{module_path}` placeholder expands to the full path from the root device to the current module, with positions joined by `/`. This is useful for modules that can be installed at any nesting depth without modification.
|
||||||
|
|
||||||
|
For example, consider an SFP module type with an interface template named `eth{module_path}`:
|
||||||
|
|
||||||
|
* Installed directly in slot 2: creates interface `eth2`
|
||||||
|
* Installed in slot 1's nested bay 1: creates interface `eth1/1`
|
||||||
|
* Installed in slot 1's nested bay 2's sub-bay 3: creates interface `eth1/2/3`
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
`{module_path}` can only be used once per template attribute, and cannot be mixed with `{module}` in the same attribute.
|
||||||
|
|
||||||
|
### Position Field Resolution
|
||||||
|
|
||||||
|
The `{module}` placeholder can also be used in the `position` field of [module bay templates](./modulebaytemplate.md) defined on a module type. This allows nested module bays to build hierarchical position values. For example, a module bay template with `position="{module}/1"`, when its parent module is installed in a bay with position "2", will have its position resolved to "2/1".
|
||||||
|
|
||||||
Automatic renaming is supported for all modular component types (those listed above).
|
Automatic renaming is supported for all modular component types (those listed above).
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
|||||||
#
|
#
|
||||||
|
|
||||||
MODULE_TOKEN = '{module}'
|
MODULE_TOKEN = '{module}'
|
||||||
|
MODULE_PATH_TOKEN = '{module_path}'
|
||||||
|
MODULE_TOKEN_SEPARATOR = '/'
|
||||||
|
|
||||||
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
MODULAR_COMPONENT_TEMPLATE_MODELS = Q(
|
||||||
app_label='dcim',
|
app_label='dcim',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from dcim.utils import resolve_module_placeholders
|
||||||
from utilities.forms import get_field_value
|
from utilities.forms import get_field_value
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -119,25 +120,47 @@ class ModuleCommonForm(forms.Form):
|
|||||||
# Get the templates for the module type.
|
# Get the templates for the module type.
|
||||||
for template in getattr(module_type, templates).all():
|
for template in getattr(module_type, templates).all():
|
||||||
resolved_name = template.name
|
resolved_name = template.name
|
||||||
|
has_module_token = MODULE_TOKEN in template.name
|
||||||
|
has_module_path_token = MODULE_PATH_TOKEN in template.name
|
||||||
|
|
||||||
# Installing modules with placeholders require that the bay has a position value
|
# Installing modules with placeholders require that the bay has a position value
|
||||||
if MODULE_TOKEN in template.name:
|
if has_module_token or has_module_path_token:
|
||||||
if not module_bay.position:
|
if not module_bay.position:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(module_bays) != template.name.count(MODULE_TOKEN):
|
# Cannot mix {module} and {module_path} in the same attribute
|
||||||
|
if has_module_token and has_module_path_token:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_(
|
_("Cannot mix {module} and {module_path} placeholders in the same template attribute.")
|
||||||
"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)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for module_bay in module_bays:
|
# Validate {module_path} - can only appear once
|
||||||
resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1)
|
if has_module_path_token:
|
||||||
|
path_token_count = template.name.count(MODULE_PATH_TOKEN)
|
||||||
|
if path_token_count > 1:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("The {module_path} placeholder can only be used once per template.")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate {module} - multi-token must match depth exactly
|
||||||
|
if has_module_token:
|
||||||
|
token_count = template.name.count(MODULE_TOKEN)
|
||||||
|
# Multiple {module} tokens must match the tree depth exactly
|
||||||
|
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} deep "
|
||||||
|
"but {tokens} placeholders given."
|
||||||
|
).format(
|
||||||
|
level=len(module_bays), tokens=token_count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use centralized helper for placeholder substitution
|
||||||
|
positions = [mb.position for mb in module_bays]
|
||||||
|
resolved_name = resolve_module_placeholders(resolved_name, positions)
|
||||||
|
|
||||||
existing_item = installed_components.get(resolved_name)
|
existing_item = installed_components.get(resolved_name)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models.base import PortMappingBase
|
from dcim.models.base import PortMappingBase
|
||||||
|
from dcim.utils import resolve_module_placeholders
|
||||||
from dcim.models.mixins import InterfaceValidationMixin
|
from dcim.models.mixins import InterfaceValidationMixin
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@@ -170,27 +171,17 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
return modules
|
return modules
|
||||||
|
|
||||||
def resolve_name(self, module):
|
def resolve_name(self, module):
|
||||||
if MODULE_TOKEN not in self.name:
|
"""Resolve {module} and {module_path} placeholders in component name."""
|
||||||
return self.name
|
|
||||||
|
|
||||||
if module:
|
if module:
|
||||||
modules = self._get_module_tree(module)
|
positions = [m.module_bay.position for m in self._get_module_tree(module)]
|
||||||
name = self.name
|
return resolve_module_placeholders(self.name, positions)
|
||||||
for module in modules:
|
|
||||||
name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
|
||||||
return name
|
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def resolve_label(self, module):
|
def resolve_label(self, module):
|
||||||
if MODULE_TOKEN not in self.label:
|
"""Resolve {module} and {module_path} placeholders in component label."""
|
||||||
return self.label
|
|
||||||
|
|
||||||
if module:
|
if module:
|
||||||
modules = self._get_module_tree(module)
|
positions = [m.module_bay.position for m in self._get_module_tree(module)]
|
||||||
label = self.label
|
return resolve_module_placeholders(self.label, positions)
|
||||||
for module in modules:
|
|
||||||
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
|
||||||
return label
|
|
||||||
return self.label
|
return self.label
|
||||||
|
|
||||||
|
|
||||||
@@ -721,11 +712,26 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
|
|||||||
verbose_name = _('module bay template')
|
verbose_name = _('module bay template')
|
||||||
verbose_name_plural = _('module bay templates')
|
verbose_name_plural = _('module bay templates')
|
||||||
|
|
||||||
|
def resolve_position(self, module):
|
||||||
|
"""
|
||||||
|
Resolve {module} and {module_path} placeholders in position field.
|
||||||
|
|
||||||
|
This allows positions like "{module}/1" to resolve to "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_placeholders(self.position, positions)
|
||||||
|
return self.position
|
||||||
|
|
||||||
def instantiate(self, **kwargs):
|
def instantiate(self, **kwargs):
|
||||||
|
module = kwargs.get('module')
|
||||||
return self.component_model(
|
return self.component_model(
|
||||||
name=self.resolve_name(kwargs.get('module')),
|
name=self.resolve_name(module),
|
||||||
label=self.resolve_label(kwargs.get('module')),
|
label=self.resolve_label(module),
|
||||||
position=self.position,
|
position=self.resolve_position(module),
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
instantiate.do_not_call_in_templates = True
|
instantiate.do_not_call_in_templates = True
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,54 @@ from django.apps import apps
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import router, transaction
|
from django.db import router, transaction
|
||||||
|
|
||||||
|
from dcim.constants import MODULE_PATH_TOKEN, MODULE_TOKEN, MODULE_TOKEN_SEPARATOR
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_module_placeholders(text, positions):
|
||||||
|
"""
|
||||||
|
Substitute {module} and {module_path} placeholders in text with position values.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: String potentially containing {module} or {module_path} placeholders
|
||||||
|
positions: List of position strings from the module tree (root to leaf)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Text with placeholders replaced according to these rules:
|
||||||
|
|
||||||
|
{module_path}: Always expands to full path (positions joined by MODULE_TOKEN_SEPARATOR).
|
||||||
|
Can only appear once in the text.
|
||||||
|
|
||||||
|
{module}: If used once, expands to the PARENT module bay position only (last in positions).
|
||||||
|
If used multiple times, each token is replaced level-by-level.
|
||||||
|
|
||||||
|
This design (Option 2 per sigprof's feedback) allows two approaches:
|
||||||
|
1. Use {module_path} for automatic full-path expansion (hardcodes '/' separator)
|
||||||
|
2. Use {module} in position fields to build custom paths with user-controlled separators
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
result = text
|
||||||
|
|
||||||
|
# Handle {module_path} - always expands to full path
|
||||||
|
if MODULE_PATH_TOKEN in result:
|
||||||
|
full_path = MODULE_TOKEN_SEPARATOR.join(positions) if positions else ''
|
||||||
|
result = result.replace(MODULE_PATH_TOKEN, full_path)
|
||||||
|
|
||||||
|
# Handle {module} - parent-only for single token, level-by-level for multiple
|
||||||
|
if MODULE_TOKEN in result:
|
||||||
|
token_count = result.count(MODULE_TOKEN)
|
||||||
|
if token_count == 1 and positions:
|
||||||
|
# Single {module}: substitute with parent (immediate) bay position only
|
||||||
|
parent_position = positions[-1] if positions else ''
|
||||||
|
result = result.replace(MODULE_TOKEN, parent_position, 1)
|
||||||
|
else:
|
||||||
|
# Multiple {module}: substitute level-by-level (existing behavior)
|
||||||
|
for pos in positions:
|
||||||
|
result = result.replace(MODULE_TOKEN, pos, 1)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def compile_path_node(ct_id, object_id):
|
def compile_path_node(ct_id, object_id):
|
||||||
return f'{ct_id}:{object_id}'
|
return f'{ct_id}:{object_id}'
|
||||||
|
|||||||
Reference in New Issue
Block a user