Coverage for  / home / runner / work / netbox-InterfaceNameRules-plugin / netbox-InterfaceNameRules-plugin / netbox-InterfaceNameRules-plugin / netbox_interface_name_rules / forms.py: 100%

70 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 15:19 +0000

1# SPDX-License-Identifier: Apache-2.0 

2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com> 

3import re 

4 

5from dcim.models import DeviceType, ModuleType, Platform 

6from django import forms 

7from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm 

8from utilities.forms.fields import CSVModelChoiceField 

9 

10from .models import InterfaceNameRule 

11 

12 

13class RuleTestForm(forms.Form): 

14 """Standalone form for previewing interface name rule output without saving.""" 

15 

16 # --- Rule definition --- 

17 module_type_is_regex = forms.BooleanField( 

18 required=False, 

19 label="Use Regex Pattern", 

20 initial=False, 

21 widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), 

22 ) 

23 module_type = forms.ModelChoiceField( 

24 queryset=ModuleType.objects.all(), 

25 required=False, 

26 label="Module Type (exact)", 

27 help_text="FK match — used when regex mode is off", 

28 widget=forms.Select(attrs={"class": "form-select"}), 

29 ) 

30 module_type_pattern = forms.CharField( 

31 required=False, 

32 label="Module Type Pattern (regex)", 

33 help_text="Regex pattern matched against ModuleType.model via re.fullmatch()", 

34 widget=forms.TextInput(attrs={"class": "form-control"}), 

35 ) 

36 parent_module_type = forms.ModelChoiceField( 

37 queryset=ModuleType.objects.all(), 

38 required=False, 

39 label="Parent Module Type", 

40 help_text="Optional: scope to modules installed inside this parent module type", 

41 widget=forms.Select(attrs={"class": "form-select"}), 

42 ) 

43 device_type = forms.ModelChoiceField( 

44 queryset=DeviceType.objects.all(), 

45 required=False, 

46 label="Device Type", 

47 help_text="Optional: scope to devices of this hardware model", 

48 widget=forms.Select(attrs={"class": "form-select"}), 

49 ) 

50 platform = forms.ModelChoiceField( 

51 queryset=Platform.objects.all(), 

52 required=False, 

53 label="Platform", 

54 help_text="Optional: scope to devices running this software platform/OS (e.g. SONiC, EOS)", 

55 widget=forms.Select(attrs={"class": "form-select"}), 

56 ) 

57 name_template = forms.CharField( 

58 required=True, 

59 label="Name Template", 

60 help_text="e.g. et-0/0/{bay_position} or {base}:{channel}", 

61 widget=forms.TextInput(attrs={"class": "form-control"}), 

62 ) 

63 channel_count = forms.IntegerField( 

64 required=False, 

65 initial=0, 

66 min_value=0, 

67 label="Channel Count", 

68 help_text="0 = no breakout; > 0 generates one interface per channel", 

69 widget=forms.NumberInput(attrs={"class": "form-control"}), 

70 ) 

71 channel_start = forms.IntegerField( 

72 required=False, 

73 initial=0, 

74 min_value=0, 

75 label="Channel Start", 

76 help_text="Starting channel index (0 for Juniper, varies for Cisco)", 

77 widget=forms.NumberInput(attrs={"class": "form-control"}), 

78 ) 

79 

80 # --- Variable override fields --- 

81 var_slot = forms.CharField( 

82 required=False, initial="1", label="{slot}", widget=forms.TextInput(attrs={"class": "form-control"}) 

83 ) 

84 var_bay_position = forms.CharField( 

85 required=False, initial="1", label="{bay_position}", widget=forms.TextInput(attrs={"class": "form-control"}) 

86 ) 

87 var_bay_position_num = forms.CharField( 

88 required=False, initial="1", label="{bay_position_num}", widget=forms.TextInput(attrs={"class": "form-control"}) 

89 ) 

90 var_parent_bay_position = forms.CharField( 

91 required=False, 

92 initial="1", 

93 label="{parent_bay_position}", 

94 widget=forms.TextInput(attrs={"class": "form-control"}), 

95 ) 

96 var_sfp_slot = forms.CharField( 

97 required=False, initial="1", label="{sfp_slot}", widget=forms.TextInput(attrs={"class": "form-control"}) 

98 ) 

99 var_base = forms.CharField( 

100 required=False, 

101 initial="Ethernet1", 

102 label="{base} (current interface name)", 

103 widget=forms.TextInput(attrs={"class": "form-control"}), 

104 ) 

105 

106 def clean(self): 

107 """Validate mutual exclusivity of regex/exact module-type fields.""" 

108 cleaned_data = super().clean() 

109 module_type_is_regex = cleaned_data.get("module_type_is_regex", False) 

110 module_type = cleaned_data.get("module_type") 

111 module_type_pattern = cleaned_data.get("module_type_pattern", "") 

112 

113 if module_type_is_regex: 

114 if not module_type_pattern: 

115 self.add_error("module_type_pattern", "A regex pattern is required when regex mode is enabled.") 

116 else: 

117 try: 

118 re.compile(module_type_pattern) 

119 except re.error as exc: 

120 self.add_error("module_type_pattern", f"Invalid regex: {exc}") 

121 else: 

122 from .models import _REDOS_PATTERN 

123 

124 if _REDOS_PATTERN.search(module_type_pattern): 

125 self.add_error("module_type_pattern", "Pattern contains potentially unsafe nested quantifiers.") 

126 if module_type: 

127 self.add_error("module_type", "Module Type (exact) must be empty when regex mode is enabled.") 

128 else: 

129 if module_type_pattern: 

130 self.add_error("module_type_pattern", "Module Type Pattern must be empty when regex mode is disabled.") 

131 

132 return cleaned_data 

133 

134 def clean_channel_count(self): 

135 """Return 0 when the field is blank or None.""" 

136 return self.cleaned_data.get("channel_count") or 0 

137 

138 def clean_channel_start(self): 

139 """Return 0 when the field is blank or None.""" 

140 return self.cleaned_data.get("channel_start") or 0 

141 

142 

143class InterfaceNameRuleForm(NetBoxModelForm): 

144 """Add/edit form for InterfaceNameRule. 

145 

146 Priority is auto-computed from the rule fields — it cannot be set manually. 

147 Scope fields (parent_module_type, device_type, platform) raise the priority score: 

148 parent_module_type +400, device_type +200, platform +100 (for regex rules). 

149 Exact FK rules always outrank regex rules (score 1000+ vs max 955). 

150 """ 

151 

152 class Meta: 

153 model = InterfaceNameRule 

154 fields = [ 

155 "module_type", 

156 "module_type_pattern", 

157 "module_type_is_regex", 

158 "parent_module_type", 

159 "device_type", 

160 "platform", 

161 "name_template", 

162 "channel_count", 

163 "channel_start", 

164 "description", 

165 "enabled", 

166 "applies_to_device_interfaces", 

167 ] 

168 help_texts = { 

169 "parent_module_type": ( 

170 "Optional. Restricts this rule to modules installed inside the given parent module type. " 

171 "Setting this raises the priority score by 400 (regex) or keeps exact priority at 1000+." 

172 ), 

173 "device_type": ( 

174 "Optional. Restricts this rule to modules installed in this device model. " 

175 "Setting this raises the priority score by 200 (regex)." 

176 ), 

177 "platform": ( 

178 "Optional. Restricts this rule to devices running this OS/platform. " 

179 "Setting this raises the priority score by 100 (regex)." 

180 ), 

181 "module_type_is_regex": ( 

182 "When checked, use a regex pattern instead of an exact FK. " 

183 "Note: exact FK rules always outrank regex rules (exact score 1000–1007, regex max 955)." 

184 ), 

185 } 

186 

187 

188class InterfaceNameRuleImportForm(NetBoxModelImportForm): 

189 """CSV/YAML bulk-import form for InterfaceNameRule.""" 

190 

191 # FK fields must declare to_field_name explicitly so YAML/CSV can reference 

192 # objects by their natural key instead of numeric PK. 

193 module_type = CSVModelChoiceField( 

194 queryset=ModuleType.objects.all(), 

195 required=False, 

196 to_field_name="model", 

197 help_text="Module type matched by its model name (e.g. SFP-10G-LR)", 

198 ) 

199 parent_module_type = CSVModelChoiceField( 

200 queryset=ModuleType.objects.all(), 

201 required=False, 

202 to_field_name="model", 

203 help_text="Parent module type matched by its model name", 

204 ) 

205 device_type = CSVModelChoiceField( 

206 queryset=DeviceType.objects.all(), 

207 required=False, 

208 to_field_name="model", 

209 help_text="Device type matched by its model name (e.g. ACX7024)", 

210 ) 

211 platform = CSVModelChoiceField( 

212 queryset=Platform.objects.all(), 

213 required=False, 

214 to_field_name="name", 

215 help_text="Platform matched by its name (e.g. SONiC)", 

216 ) 

217 

218 class Meta: 

219 model = InterfaceNameRule 

220 fields = [ 

221 "module_type", 

222 "module_type_pattern", 

223 "module_type_is_regex", 

224 "parent_module_type", 

225 "device_type", 

226 "platform", 

227 "name_template", 

228 "channel_count", 

229 "channel_start", 

230 "description", 

231 "enabled", 

232 "applies_to_device_interfaces", 

233 ] 

234 

235 

236class InterfaceNameRuleFilterForm(NetBoxModelFilterSetForm): 

237 """Filter form for the InterfaceNameRule list view.""" 

238 

239 q = forms.CharField(required=False, label="Search") 

240 module_type_id = forms.ModelChoiceField( 

241 queryset=ModuleType.objects.all(), 

242 required=False, 

243 label="Module Type", 

244 ) 

245 module_type_is_regex = forms.NullBooleanField(required=False, label="Regex Mode") 

246 applies_to_device_interfaces = forms.NullBooleanField(required=False, label="Device Interface Rules") 

247 enabled = forms.NullBooleanField(required=False, label="Enabled") 

248 module_type_pattern = forms.CharField(required=False, label="Pattern (contains)") 

249 parent_module_type_id = forms.ModelChoiceField( 

250 queryset=ModuleType.objects.all(), 

251 required=False, 

252 label="Parent Module Type", 

253 ) 

254 device_type_id = forms.ModelChoiceField( 

255 queryset=DeviceType.objects.all(), 

256 required=False, 

257 label="Device Type", 

258 ) 

259 platform_id = forms.ModelChoiceField( 

260 queryset=Platform.objects.all(), 

261 required=False, 

262 label="Platform", 

263 ) 

264 

265 model = InterfaceNameRule