- Document that ModuleType.to_yaml() exports module_bay_types by name but
the field isn't currently importable back through it (no ModuleTypeImportForm
field survived the CSV-import revert).
- modulebaytemplate.md's note covered only the device-type-parented import
path; ModuleBayTemplateImportForm is registered for both DeviceTypeImportView
and ModuleTypeImportView, scoping to whichever parent type's manufacturer
applies. Reworded to cover both, and added the "rejected rather than
resolved" clause for a name matching only some other manufacturer's type.
- Clarified clean_module_bay_types()'s docstring: the "never a cross-manufacturer
collision" guarantee holds only because ModularComponentTemplateModel.clean()
rejects a template with neither device_type nor module_type before this
method's result would ever be saved.
- Fixed a test docstring overstating symmetry between its two comparison arms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per review, ditch dedupe_module_bay_types_by_manufacturer() and any import
logic that resolves module_bay_types by name alone across manufacturers.
ModuleBayType's unique constraint is (manufacturer, name), not name alone,
so resolving a bare name against an unscoped, potentially cross-manufacturer
pool -- guessing via a preference order, rejecting only on a genuine tie --
is not a reliable way to identify a specific object. CSV import forms have
no way to qualify an M2M reference beyond a bare name, so module_bay_types
is no longer exposed there at all (ModuleTypeImportForm, ModuleBayImportForm
in bulk_import.py): it's acceptable not to support this rather than resolve
it unreliably. This also reverts the netbox/tables/columns.py export_transform
API addition and the three tables' use of it, which existed only to make the
CSV round trip work.
The one import path that survives is ModuleBayTemplateImportForm (the YAML
device/module type "Import Components" flow), because it can reliably scope
module_bay_types' queryset to the parent device/module type's own
manufacturer plus global (manufacturer-less) types *before* resolving by
name -- so a name collision is never cross-manufacturer, only "this
manufacturer's own type vs. a global one of the same name," which
ModuleBayType's own uniqueness constraint makes unambiguous. A name matching
only some other manufacturer's type doesn't resolve at all, rather than
being coerced to an arbitrary guess.
Kept: the ModuleBayTemplateImportForm.enabled field/clean_enabled() fix
(default=True was previously lost on YAML re-import; unrelated to the
above), and to_yaml()'s export of module_bay_types on both ModuleType and
ModuleBayTemplate, plus the export_yaml() prefetch optimizations -- none of
these involve resolving an object's identity from an ambiguous attribute.
Trimmed the model docs to match: the modulebay.md and moduletype.md
paragraphs described capabilities (CSV import, cross-manufacturer YAML
import) that no longer exist and are removed; modulebaytemplate.md's note
is rewritten to describe the actual (manufacturer-or-global-scoped)
resolution behavior.
Allow channel subinterfaces to retain a specific physical interface type
and rename conventionally named children when their parent is renamed.
Keep mirrored cable and path state consistent when channel bindings
change, avoid unnecessary path rebuilds, and apply the same rename
behavior to interface templates.
- ModuleBayType.__str__() includes the manufacturer (e.g. "Cisco SFP28"),
but the three module_bay_types ManyToManyColumn declarations had no
transform, so django-tables2 defaulted to str() for CSV export while
the import forms resolve by name alone. Verified directly: exporting
a manufacturer-scoped bay type produced "Cisco SFP28", which then
failed to re-import with "Object not found: Cisco SFP28" -- broken
for exactly the case (manufacturer-scoped types) the preference/
ambiguity machinery exists to serve. Set transform=lambda obj: obj.name
on all three columns to match to_yaml(), and rewrote the existing
round-trip test to use a manufacturer-scoped bay type instead of a
global one, which is the only case that exercised str().
- The three ambiguity-refusal tests asserted only that the field
errored, which a plain invalid_choice (e.g. from a queryset that
excluded both candidates) would also satisfy -- masking a regression
of the manufacturer scoping removed two commits ago. Tightened each to
assert the error names both competing manufacturers.
- Corrected modulebay.md, which still described module_bay_types
resolution as "scoped to" the device's manufacturer -- the behavior
the prior commit removed as a bug; it's a preference, not a scope.
- Trimmed comments and docstrings introduced across this branch to a
more proportionate length.
Deliberately out of scope for this PR (tracked as follow-up
considerations, not fixed here): an escape hatch for a bay type name
that's genuinely ambiguous across manufacturers with no local match
(would require a new wire-format convention), and ModuleType.to_yaml()
not exporting a module-bays section at all (a separate, pre-existing
asymmetry, larger than this PR's scope).
- CSVModelMultipleChoiceField.clean() split on a bare comma with no
whitespace stripping, but ManyToManyColumn's default CSV export
separator is ", " (comma + space) -- so re-importing NetBox's own CSV
export of any multi-value column using this field (module_bay_types
among others, since this is a shared utility field) failed with
"Object not found: <value>" on every value after the first. Verified
directly against ModuleTypeTable's actual export value before fixing.
Also cast to str() before splitting: a YAML-bound caller (as opposed to
a CSV cell, always a string) can pass a non-string scalar, which
previously raised an unhandled AttributeError instead of a form error.
- Docs for module bay type resolution still described the pre-a3b5e4b
fallback ("then any remaining candidate"); updated to describe the
refusal behavior that replaced it. Added a matching note to
modulebay.md, which had none.
- dedupe_module_bay_types_by_manufacturer() collapses candidates by pk
within each name group before computing preference, so a caller
passing a duplicate row in a raw list -- the signature accepts "an
iterable," not just a queryset -- can't manufacture a same-manufacturer
tie that would then crash on None.manufacturer.name. Unreachable via
the three current callers today (each resolves from a queryset,
which can't contain a row twice), but cheap to make the helper safe
standalone.
- Fixed a stale test docstring contrasting the two import forms' field
types by a distinction (plain vs. CSV multiple-choice field) that no
longer exists since both were aligned to CSVModelMultipleChoiceField.
- Added ambiguity-refusal coverage at the other two call sites
(ModuleBayTemplateImportForm, ModuleBayImportForm) -- previously only
ModuleTypeImportForm was covered for this path.
Also found independently while verifying the above: ModuleTypeListView
.export_yaml() prefetched modulebaytemplates__module_bay_types, but
ModuleType.to_yaml() -- unlike DeviceType.to_yaml() -- never reads
self.modulebaytemplates at all (a separate, pre-existing, out-of-scope
gap: ModuleType.to_yaml() doesn't export a nested module-bays section).
That prefetch was dead weight, adding a query with no corresponding
saving. Removed it, and with it the now-meaningless "bay count doesn't
affect query count" test (nothing in ModuleType.to_yaml() ever varied
with bay count to begin with), replacing it with an exact-delta
assertion isolating what the one relevant prefetch (module_bay_types
on the module type itself) actually saves.
The manufacturer-or-null queryset scoping added to disambiguate a name shared
by a global and a manufacturer-scoped ModuleBayType went further than
intended: it also excluded a *different* manufacturer's bay type entirely.
The UI (ModuleTypeForm/ModuleBayTemplateForm) and REST API place no such
restriction -- a third-party module may legitimately declare compatibility
with another manufacturer's proprietary bay type. Confirmed the regression
directly: creating that assignment via ModuleTypeForm succeeds, but
exporting it and re-importing the same YAML failed with
"Object not found: SFP28", making valid existing data unimportable -- worse
than the bug this feature exists to fix.
Remove the queryset scoping entirely and instead make
dedupe_module_bay_types_by_manufacturer() manufacturer-aware: given the
target manufacturer, it now prefers (in order) an exact match, then a global
type, then any remaining candidate, resolved from an unscoped queryset in
clean() rather than a sibling clean_<field>() mutating the field's queryset.
This also drops the Meta.fields-ordering dependency those methods required.
Also, from the same review round:
- Test asserting Django's literal English error string now asserts the
error code instead, so it survives wording changes/translation.
- The prefetch query-count test moved from test_models.py (which doesn't
otherwise touch views) to test_views.py, and strengthened from "prefetch
saves at least one query" to "query count is constant regardless of bay
count" -- the actual invariant. Added equivalent coverage for
ModuleTypeListView, which the prior version didn't test at all.
- Corrected the export_yaml() prefetch comments to not imply the other
~11 relations to_yaml() touches are also covered -- they aren't, and
weren't before this feature either.
- Updated the model docs to describe the new (permissive, cross-manufacturer
allowed) behavior instead of the old (restrictive) one they described a
commit ago.
Adds regression tests importing a bay type belonging to a different
manufacturer than the importing device/module type, through both
ModuleBayTemplateImportForm and ModuleTypeImportForm.
- clean_module_bay_types()'s two ValidationErrors were unreachable:
ModelMultipleChoiceField.clean() already raises before the clean_<name>
hook runs on a non-list or an unresolvable name, per Django's
BaseForm._clean_fields(). Simplify to dedupe from cleaned_data (already
scoped and validated) via a shared dedupe_module_bay_types_by_manufacturer()
helper in dcim/utils.py, used by both ModuleBayTemplateImportForm and the
new ModuleTypeImportForm.module_bay_types below. This also drops the
self.data access that ignored the form prefix, broke on a QueryDict, and
re-queried once per name.
- ModuleType.module_bay_types (the module's own side of the bay/module
compatibility intersection) was still missing from the YAML round trip.
Add it to ModuleType.to_yaml() and ModuleTypeImportForm, mirroring
ModuleBayTemplateImportForm's manufacturer-scoping and dedup.
- ModuleBayTemplate.to_yaml() emitted enabled but the import form didn't
accept it, so it silently reset to False (not the model's default=True)
on any dict-bound re-import. Add it with the same
clean_enabled()-defaults-to-True pattern already used by
ModuleBayImportForm's CSV import.
- Prefetch module_bay_types in DeviceTypeListView/ModuleTypeListView's
export_yaml() so bulk YAML export doesn't add one query per module bay
template across the exported queryset.
- Document the manufacturer-preference precedence rule in the model docs,
since export emits a bare name and import can resolve a colliding one to
either a global or manufacturer-specific type.
Adds regression tests for the module_type-scoped path, the enabled
round trip, an export/import round trip, export ordering, the new
ModuleTypeImportForm coverage, and the prefetch fix.
* Add support for liquid cooling components
* Include sample of offending components when module move is disallowed
* Use settings.BULK_UPDATE_CHUNK_SIZE for batch_size
* Adopt review feedback
* Implement {module} position inheritance for nested module bays (#19796)
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 (e.g. {module}/1 vs {module}-1 vs {module}.1).
Model layer:
- Add _get_inherited_positions() to resolve {module} in positions as
the module tree is walked from root to leaf
- Update _resolve_module_placeholder() with single-token logic: one
{module} resolves to the leaf bay's inherited position; multi-token
continues level-by-level replacement for backwards compatibility
Form layer:
- Update _get_module_bay_tree() to resolve {module} in positions during
traversal, propagating parent positions through the tree
- Extract validation into _validate_module_tokens() private method
Tests:
- Position inheritance at depth 2 and 3
- Custom separator (dot notation)
- Multi-token backwards compatibility
- Documentation for position inheritance
Fixes: #19796
* Consolidate {module} placeholder logic into shared utilities and add API validation
Extract get_module_bay_positions() and resolve_module_placeholder() into
dcim/utils.py as shared routines used by the model, form, and API serializer.
This eliminates duplicated traversal and resolution logic across three layers.
Key changes:
- Add position inheritance: {module} tokens in bay position fields resolve
using the parent bay's position during hierarchy traversal
- Single {module} token now resolves to the leaf bay's inherited position
- Mismatched token count vs tree depth now raises ValueError instead of
silently producing partial strings
- API serializer validation uses shared utilities for parity with the form
- Fix error message wording ("levels deep" instead of "in tree")
Add an `enabled` boolean field to ModuleBay, ModuleBayTemplate,
DeviceBay, and DeviceBayTemplate models. Disabled bays prevent component
installation and display accordingly in the UI. Update serializers,
filters, forms, and tables to support the new field.
Fixes#20152
* Move Module & ModuleType models to a separate file
* Add ModuleTypeProfile & related fields
* Initial work on JSON schema validation
* Add attributes property on ModuleType
* Introduce MultipleOfValidator
* Introduce JSONSchemaProperty
* Enable dynamic form field rendering
* Misc cleanup
* Fix migration conflict
* Ensure deterministic ordering of attriubte fields
* Support choices & default values
* Include module type attributes on module view
* Enable modifying individual attributes via REST API
* Enable filtering by attribute values
* Add documentation & tests
* Schema should be optional
* Include attributes column for profiles
* Profile is nullable
* Include some initial profiles to be installed via migration
* Fix migrations conflict
* Fix filterset test
* Misc cleanup
* Fixes#19023: get_field_value() should respect null values in bound forms (#19024)
* Skip filters which do not specify a JSON-serializable value
* Fix handling of array item types
* Fix initial data in schema field during bulk edit
* Implement sanity checking for JSON schema definitions
* Fall back to filtering by string value
Made DeviceRoles hierarchical, had to also change the filtersets for Device, ConfigContext and VirtualMachine to use the TreeNodeMultipleChoiceFilter.
Note: The model was changed to use NestedGroupModel, a side-effect of this is it also adds comments field, but I thought that was better then doing a one-off just for DeviceRole and having to define the fields, validators, etc.. - keeps everything DRY / consistent.
* 18981 Make Device Roles Hierarchical
* 18981 forms, serializer
* 18981 fix tests
* 18981 fix tests
* 18981 fix tests
* 18981 fix tests
* 18981 fix tests
* 18981 fix migration merge
* 18981 fix tests
* 18981 fix filtersets
* 18981 fix tests
* 18981 comments
* 18981 review changes
* Create MACAddress model and migrations to convert existing .mac_address fields to standalone objects
* Add migrations
* All views/filtering working and documentation done; no unit tests yet
* Redo migrations following VLAN Translation
* Remove mac_address filter fields and add table columns for device/vm
* Remove unnecessary "bulk rename"
* Fix filterset tests for Device
* Fix filterset tests for Interface
* Fix tests on single-object forms
* Fix serializer tests
* Fix filterset tests for VMInterface
* Fix filterset tests for Device and VirtualMachine
* Move new field check into lookup_map iteration
* Fix general MACAddress filter tests
* Add GraphQL types/filters/schema
* Fix bulk edit/create tests (bulk editing Interfaces will be unsupported because of inheritance from ComponentBulkEditForm)
* Make mac_address read_only on InterfaceSerializer/VMInterfaceSerializer
* Undo unrelated work
* Cleanup unused IPAddress derived stuff
* API endpoints
* Add serializer objects to interface serializers
* Clean up unnecessary bulk create forms/views/routes
* Add SearchIndex and adjust indexable fields for Interface and VMInterface
* Reorganize MACAddress classes out of association with DeviceComponents
* Move MACAddressSerializer
* Enforce saving only a single is_primary MACAddress per interface/vminterface
* Perform is_primary validation on MACAddress model and just check if one already exists for the interface
* Remove form-level validation
* Fix check for current is_primary setting when reassigning
* Model cleanup
* Documentation notes and cleanup
* Simplify serializer and add ip_addresses
* Add to VMInterfaceSerializer too
* Style cleanup
* Standardize "MAC Address" instead of "MAC"
* Remove unused views
* Add is_primary field for bulk edit
* HTML cleanup and add copy-to-clipboard button
* Remove mac_address from Interface and VMInterface bulk-edit forms
* Add device and VM filtering
* Use combined assigned_object_parent in table to match structure of IPAddressTable
* Add GFK fields to MACAddressSerializer
* Reorganize "Addressing" sections to remove from proximity to "Device Components" and related groupings
* Clean up migrations
* Misc cleanup
* Add filterset test
* Remove mac_address field from interface forms
* Designate primary MAC address via a ForeignKey on the interface models
* Add serializer fields for primary_mac_address
* Update docs
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* VLANTranslationPolicy and VLANTranslationRule models and all associated UI classes
* Change VLANTranslationPolicy to a PrimaryModel and make name unique
* Add serializer classes to InterfaceSerializer
* Remake migrations
* Add GraphQL typing
* Skip tagged models in test
* Missing migration
* Remove get_absolute_url methods
* Remove package-lock.json
* Rebuild migration and add constraints and field options
* Rebuild migrations
* Use DynamicModelChoiceField for policy field
* Make vlan_translation_policy fields on filtersets more consistent with existing __name convention
* Add vlan_translation_table to VMInterface detail page
* Add vlan_translation_policy to VMInterfaceSerializer
* Move vlan_translation_policy fields to model and filterset mixins
* Protect in-use policies against deletion
* Add vlan_translation_policy to fields in VMInterfaceSerializer
* Cleanup indentation
* Remove unnecessary ordering column
* Rebuild migrations
* Search methods and registration
* Ensure 'id' column is present by default
* Add graphql types/filters/schema for VLANTranslationRule
* Filterset tests
* View tests
* API and viewset tests (incomplete)
* Add tags to VLANTranslationRuleForm
* Complete viewset tests for VLANTranslationRule
* Make VLANTranslationRule.policy nullable (but still required)
* Revert "Make VLANTranslationRule.policy nullable (but still required)"
This reverts commit 4c1bb437ef.
* Revert nullability
* Explicitly prefetch policy in graphql
* Documentation of new and affected models
* Add note about select_related in graphql
* Rework policy/rule documentation
* Move vlan_translation_policy into 802.1Q Switching fieldset
* Remove redundant InterfaceVLANTranslationTable
* Conditionally include vlan_translation_table in interface.html and vminterface.html
* Add description field to VLANTranslationRule
* Define vlan_translation_table conditionally
* Add policy (name) filter to VLANTranslationRuleFilterSet
* Revert changes to adding-models.md (moved to another PR)
* Dynamic table for linked rules in vlantranslationpolicy.html
* Misc cleanup
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* 17195 Add color to PowerOutlet
* 17195 Add color to PowerOutlet
* Reorder serializer fields
* Misc cleanup
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>