Coverage for dcim/forms/common.py: 14%
94 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django import forms
2from django.core.exceptions import ValidationError
3from django.utils.translation import gettext_lazy as _
4from netaddr import EUI, AddrFormatError
6from dcim.choices import *
7from dcim.constants import *
8from dcim.utils import get_module_bay_positions, resolve_module_placeholder
9from netbox.context import current_request
10from utilities.exceptions import AbortRequest
11from utilities.forms import get_field_value
13__all__ = (
14 'InterfaceCommonForm',
15 'ModuleCommonForm'
16)
19class InterfaceCommonForm(forms.Form):
20 mtu = forms.IntegerField(
21 required=False,
22 min_value=INTERFACE_MTU_MIN,
23 max_value=INTERFACE_MTU_MAX,
24 label=_('MTU')
25 )
26 mac_address = forms.CharField(
27 required=False,
28 empty_value=None,
29 label=_('MAC address'),
30 help_text=_('Enter a MAC address to create and assign it as the primary MAC in one step.')
31 )
33 def __init__(self, *args, **kwargs):
34 super().__init__(*args, **kwargs)
36 # Determine the selected 802.1Q mode
37 interface_mode = get_field_value(self, 'mode')
39 # Delete VLAN tagging fields which are not relevant for the selected mode
40 if interface_mode in (InterfaceModeChoices.MODE_ACCESS, InterfaceModeChoices.MODE_TAGGED_ALL):
41 del self.fields['tagged_vlans']
42 elif not interface_mode:
43 del self.fields['vlan_group']
44 del self.fields['untagged_vlan']
45 del self.fields['tagged_vlans']
46 if interface_mode != InterfaceModeChoices.MODE_Q_IN_Q:
47 del self.fields['qinq_svlan']
49 if self.instance and self.instance.pk and self.instance.primary_mac_address:
50 # Pre-populate mac_address with the current primary MAC string so it round-trips cleanly
51 self.fields['mac_address'].initial = str(self.instance.primary_mac_address.mac_address)
53 def clean(self):
54 super().clean()
56 mac_address = self.cleaned_data.get('mac_address')
57 if mac_address:
58 try:
59 EUI(mac_address, version=48)
60 except (AddrFormatError, ValueError, TypeError):
61 raise forms.ValidationError({
62 'mac_address': _('Enter a valid MAC address (e.g. 00:11:22:33:44:55).')
63 })
64 # Require add_macaddress only when the field is actually being changed (a MAC may need to be
65 # created). A pre-populated primary MAC left untouched must not gate unrelated edits.
66 if 'mac_address' in self.changed_data:
67 request = current_request.get()
68 if request is not None and not request.user.has_perm('dcim.add_macaddress'):
69 raise forms.ValidationError({
70 'mac_address': _('You do not have permission to create MAC addresses.')
71 })
72 parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
73 if 'tagged_vlans' in self.fields.keys():
74 tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \
75 self.get_initial_for_field(self.fields['tagged_vlans'], 'tagged_vlans')
76 else:
77 tagged_vlans = []
79 # Validate tagged VLANs; must be a global VLAN or in the same site
80 if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
81 valid_sites = [None, self.cleaned_data[parent_field].site]
82 invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
84 if invalid_vlans:
85 raise forms.ValidationError({
86 'tagged_vlans': _(
87 "The tagged VLANs ({vlans}) must belong to the same site as the interface's parent device/VM, "
88 "or they must be global"
89 ).format(vlans=', '.join(invalid_vlans))
90 })
91 # Validate mode change
92 if self.instance.pk and (self.instance.mode != self.cleaned_data['mode']):
93 if 'untagged_vlan' not in self.cleaned_data and self.instance.untagged_vlan is not None:
94 self.instance.untagged_vlan = None
95 if 'tagged_vlans' not in self.cleaned_data and self.instance.tagged_vlans is not None:
96 self.instance.tagged_vlans.clear()
98 def save(self, commit=True):
99 instance = super().save(commit=commit)
101 if commit and 'mac_address' in self.changed_data:
102 try:
103 instance.set_primary_mac_address_from_value(self.cleaned_data.get('mac_address'))
104 except ValidationError as e:
105 # Surface a model/custom validation failure (e.g. a MACAddress CustomValidator) as a
106 # clean request abort rather than letting it escape as a 500.
107 raise AbortRequest('; '.join(e.messages))
109 return instance
112class ModuleCommonForm(forms.Form):
114 def clean(self):
115 super().clean()
117 replicate_components = self.cleaned_data.get('replicate_components')
118 adopt_components = self.cleaned_data.get('adopt_components')
119 device = self.cleaned_data.get('device')
120 module_type = self.cleaned_data.get('module_type')
121 module_bay = self.cleaned_data.get('module_bay')
123 if adopt_components:
124 self.instance._adopt_components = True
126 # Bail out if we are not installing a new module or if we are not replicating components (or if
127 # validation has already failed)
128 if self.errors or self.instance.pk or not replicate_components:
129 self.instance._disable_replication = True
130 return
132 try:
133 positions = get_module_bay_positions(module_bay)
134 except ValueError as e:
135 raise forms.ValidationError(str(e))
137 for templates, component_attribute in [
138 ("consoleporttemplates", "consoleports"),
139 ("consoleserverporttemplates", "consoleserverports"),
140 ("interfacetemplates", "interfaces"),
141 ("powerporttemplates", "powerports"),
142 ("poweroutlettemplates", "poweroutlets"),
143 ("coolingintaketemplates", "coolingintakes"),
144 ("coolingoutflowtemplates", "coolingoutflows"),
145 ("rearporttemplates", "rearports"),
146 ("frontporttemplates", "frontports")
147 ]:
148 # Prefetch installed components
149 installed_components = {
150 component.name: component for component in getattr(device, component_attribute).all()
151 }
153 # Get the templates for the module type.
154 for template in getattr(module_type, templates).all():
155 resolved_name = template.name
156 if MODULE_TOKEN in template.name:
157 if not module_bay.position:
158 raise forms.ValidationError(
159 _("Cannot install module with placeholder values in a module bay with no position defined.")
160 )
161 try:
162 resolved_name = resolve_module_placeholder(template.name, positions)
163 except ValueError as e:
164 raise forms.ValidationError(str(e))
166 existing_item = installed_components.get(resolved_name)
168 # It is not possible to adopt components already belonging to a module
169 if adopt_components and existing_item and existing_item.module:
170 raise forms.ValidationError(
171 _("Cannot adopt {model} {name} as it already belongs to a module").format(
172 model=template.component_model.__name__,
173 name=resolved_name
174 )
175 )
177 # If we are not adopting components we error if the component exists
178 if not adopt_components and resolved_name in installed_components:
179 raise forms.ValidationError(
180 _("A {model} named {name} already exists").format(
181 model=template.component_model.__name__,
182 name=resolved_name
183 )
184 )