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

1from django import forms 

2from django.core.exceptions import ValidationError 

3from django.utils.translation import gettext_lazy as _ 

4from netaddr import EUI, AddrFormatError 

5 

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 

12 

13__all__ = ( 

14 'InterfaceCommonForm', 

15 'ModuleCommonForm' 

16) 

17 

18 

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 ) 

32 

33 def __init__(self, *args, **kwargs): 

34 super().__init__(*args, **kwargs) 

35 

36 # Determine the selected 802.1Q mode 

37 interface_mode = get_field_value(self, 'mode') 

38 

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'] 

48 

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) 

52 

53 def clean(self): 

54 super().clean() 

55 

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 = [] 

78 

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] 

83 

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() 

97 

98 def save(self, commit=True): 

99 instance = super().save(commit=commit) 

100 

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)) 

108 

109 return instance 

110 

111 

112class ModuleCommonForm(forms.Form): 

113 

114 def clean(self): 

115 super().clean() 

116 

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') 

122 

123 if adopt_components: 

124 self.instance._adopt_components = True 

125 

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 

131 

132 try: 

133 positions = get_module_bay_positions(module_bay) 

134 except ValueError as e: 

135 raise forms.ValidationError(str(e)) 

136 

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 } 

152 

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)) 

165 

166 existing_item = installed_components.get(resolved_name) 

167 

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 ) 

176 

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 )