forms.py 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178
  1. import re
  2. from django import forms
  3. from django.db.models import Count, Q
  4. from ipam.models import IPAddress
  5. from utilities.forms import (
  6. APISelect, BootstrapMixin, BulkImportForm, CommentField, ConfirmationForm, CSVDataField, ExpandableNameField,
  7. FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
  8. )
  9. from .models import (
  10. DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
  11. ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
  12. Interface, IFACE_FF_VIRTUAL, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
  13. PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
  14. )
  15. FORM_STATUS_CHOICES = [
  16. ['', '---------'],
  17. ]
  18. FORM_STATUS_CHOICES += STATUS_CHOICES
  19. DEVICE_BY_PK_RE = '{\d+\}'
  20. def get_device_by_name_or_pk(name):
  21. """
  22. Attempt to retrieve a device by either its name or primary key ('{pk}').
  23. """
  24. if re.match(DEVICE_BY_PK_RE, name):
  25. pk = name.strip('{}')
  26. device = Device.objects.get(pk=pk)
  27. else:
  28. device = Device.objects.get(name=name)
  29. return device
  30. #
  31. # Sites
  32. #
  33. class SiteForm(forms.ModelForm, BootstrapMixin):
  34. slug = SlugField()
  35. comments = CommentField()
  36. class Meta:
  37. model = Site
  38. fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
  39. widgets = {
  40. 'physical_address': SmallTextarea(attrs={'rows': 3}),
  41. 'shipping_address': SmallTextarea(attrs={'rows': 3}),
  42. }
  43. help_texts = {
  44. 'name': "Full name of the site",
  45. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  46. 'asn': "BGP autonomous system number",
  47. 'physical_address': "Physical location of the building (e.g. for GPS)",
  48. 'shipping_address': "If different from the physical address"
  49. }
  50. class SiteFromCSVForm(forms.ModelForm):
  51. class Meta:
  52. model = Site
  53. fields = ['name', 'slug', 'facility', 'asn']
  54. class SiteImportForm(BulkImportForm, BootstrapMixin):
  55. csv = CSVDataField(csv_form=SiteFromCSVForm)
  56. #
  57. # Rack groups
  58. #
  59. class RackGroupForm(forms.ModelForm, BootstrapMixin):
  60. slug = SlugField()
  61. class Meta:
  62. model = RackGroup
  63. fields = ['site', 'name', 'slug']
  64. class RackGroupBulkDeleteForm(ConfirmationForm):
  65. pk = forms.ModelMultipleChoiceField(queryset=RackGroup.objects.all(), widget=forms.MultipleHiddenInput)
  66. def rackgroup_site_choices():
  67. site_choices = Site.objects.annotate(rack_count=Count('rack_groups'))
  68. return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
  69. class RackGroupFilterForm(forms.Form, BootstrapMixin):
  70. site = forms.MultipleChoiceField(required=False, choices=rackgroup_site_choices,
  71. widget=forms.SelectMultiple(attrs={'size': 8}))
  72. #
  73. # Racks
  74. #
  75. class RackForm(forms.ModelForm, BootstrapMixin):
  76. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
  77. api_url='/api/dcim/rack-groups/?site_id={{site}}',
  78. ))
  79. comments = CommentField()
  80. class Meta:
  81. model = Rack
  82. fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
  83. help_texts = {
  84. 'site': "The site at which the rack exists",
  85. 'name': "Organizational rack name",
  86. 'facility_id': "The unique rack ID assigned by the facility",
  87. 'u_height': "Height in rack units",
  88. }
  89. widgets = {
  90. 'site': forms.Select(attrs={'filter-for': 'group'}),
  91. }
  92. def __init__(self, *args, **kwargs):
  93. super(RackForm, self).__init__(*args, **kwargs)
  94. # Limit rack group choices
  95. if self.is_bound and self.data.get('site'):
  96. self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
  97. elif self.initial.get('site'):
  98. self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
  99. else:
  100. self.fields['group'].choices = []
  101. class RackFromCSVForm(forms.ModelForm):
  102. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
  103. error_messages={'invalid_choice': 'Site not found.'})
  104. group_name = forms.CharField(required=False)
  105. class Meta:
  106. model = Rack
  107. fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
  108. def clean(self):
  109. site = self.cleaned_data.get('site')
  110. group = self.cleaned_data.get('group_name')
  111. # Validate rack group
  112. if site and group:
  113. try:
  114. self.instance.group = RackGroup.objects.get(site=site, name=group)
  115. except RackGroup.DoesNotExist:
  116. self.add_error('group_name', "Invalid rack group ({})".format(group))
  117. class RackImportForm(BulkImportForm, BootstrapMixin):
  118. csv = CSVDataField(csv_form=RackFromCSVForm)
  119. class RackBulkEditForm(forms.Form, BootstrapMixin):
  120. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  121. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  122. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
  123. u_height = forms.IntegerField(required=False, label='Height (U)')
  124. comments = CommentField()
  125. class RackBulkDeleteForm(ConfirmationForm):
  126. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  127. def rack_site_choices():
  128. site_choices = Site.objects.annotate(rack_count=Count('racks'))
  129. return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
  130. def rack_group_choices():
  131. group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
  132. return [(g.pk, '{} ({})'.format(g, g.rack_count)) for g in group_choices]
  133. class RackFilterForm(forms.Form, BootstrapMixin):
  134. site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
  135. widget=forms.SelectMultiple(attrs={'size': 8}))
  136. group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
  137. widget=forms.SelectMultiple(attrs={'size': 8}))
  138. #
  139. # Manufacturers
  140. #
  141. class ManufacturerForm(forms.ModelForm, BootstrapMixin):
  142. slug = SlugField()
  143. class Meta:
  144. model = Manufacturer
  145. fields = ['name', 'slug']
  146. class ManufacturerBulkDeleteForm(ConfirmationForm):
  147. pk = forms.ModelMultipleChoiceField(queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput)
  148. #
  149. # Device types
  150. #
  151. class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
  152. slug = SlugField(slug_source='model')
  153. class Meta:
  154. model = DeviceType
  155. fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
  156. 'is_network_device', 'subdevice_role']
  157. class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
  158. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  159. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
  160. u_height = forms.IntegerField(min_value=1, required=False)
  161. class DeviceTypeBulkDeleteForm(ConfirmationForm):
  162. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  163. def devicetype_manufacturer_choices():
  164. manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
  165. return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
  166. class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
  167. manufacturer = forms.MultipleChoiceField(required=False, choices=devicetype_manufacturer_choices,
  168. widget=forms.SelectMultiple(attrs={'size': 8}))
  169. #
  170. # Device component templates
  171. #
  172. class ConsolePortTemplateForm(forms.ModelForm, BootstrapMixin):
  173. name_pattern = ExpandableNameField(label='Name')
  174. class Meta:
  175. model = ConsolePortTemplate
  176. fields = ['name_pattern']
  177. class ConsoleServerPortTemplateForm(forms.ModelForm, BootstrapMixin):
  178. name_pattern = ExpandableNameField(label='Name')
  179. class Meta:
  180. model = ConsoleServerPortTemplate
  181. fields = ['name_pattern']
  182. class PowerPortTemplateForm(forms.ModelForm, BootstrapMixin):
  183. name_pattern = ExpandableNameField(label='Name')
  184. class Meta:
  185. model = PowerPortTemplate
  186. fields = ['name_pattern']
  187. class PowerOutletTemplateForm(forms.ModelForm, BootstrapMixin):
  188. name_pattern = ExpandableNameField(label='Name')
  189. class Meta:
  190. model = PowerOutletTemplate
  191. fields = ['name_pattern']
  192. class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
  193. name_pattern = ExpandableNameField(label='Name')
  194. class Meta:
  195. model = InterfaceTemplate
  196. fields = ['name_pattern', 'form_factor', 'mgmt_only']
  197. class DeviceBayTemplateForm(forms.ModelForm, BootstrapMixin):
  198. name_pattern = ExpandableNameField(label='Name')
  199. class Meta:
  200. model = DeviceBayTemplate
  201. fields = ['name_pattern']
  202. #
  203. # Device roles
  204. #
  205. class DeviceRoleForm(forms.ModelForm, BootstrapMixin):
  206. slug = SlugField()
  207. class Meta:
  208. model = DeviceRole
  209. fields = ['name', 'slug', 'color']
  210. class DeviceRoleBulkDeleteForm(ConfirmationForm):
  211. pk = forms.ModelMultipleChoiceField(queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput)
  212. #
  213. # Platforms
  214. #
  215. class PlatformForm(forms.ModelForm, BootstrapMixin):
  216. slug = SlugField()
  217. class Meta:
  218. model = Platform
  219. fields = ['name', 'slug']
  220. class PlatformBulkDeleteForm(ConfirmationForm):
  221. pk = forms.ModelMultipleChoiceField(queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput)
  222. #
  223. # Devices
  224. #
  225. class DeviceForm(forms.ModelForm, BootstrapMixin):
  226. site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
  227. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
  228. api_url='/api/dcim/racks/?site_id={{site}}',
  229. display_field='display_name',
  230. attrs={'filter-for': 'position'}
  231. ))
  232. position = forms.TypedChoiceField(required=False, empty_value=None,
  233. help_text="For multi-U devices, this is the lowest occupied rack unit.",
  234. widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
  235. disabled_indicator='device'))
  236. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
  237. widget=forms.Select(attrs={'filter-for': 'device_type'}))
  238. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
  239. api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
  240. display_field='model'
  241. ))
  242. comments = CommentField()
  243. class Meta:
  244. model = Device
  245. fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
  246. 'platform', 'primary_ip', 'comments']
  247. help_texts = {
  248. 'device_role': "The function this device serves",
  249. 'serial': "Chassis serial number",
  250. }
  251. widgets = {
  252. 'face': forms.Select(attrs={'filter-for': 'position'}),
  253. 'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
  254. }
  255. def __init__(self, *args, **kwargs):
  256. super(DeviceForm, self).__init__(*args, **kwargs)
  257. if self.instance.pk:
  258. # Initialize helper selections
  259. self.initial['site'] = self.instance.rack.site
  260. self.initial['manufacturer'] = self.instance.device_type.manufacturer
  261. # Compile list of IPs assigned to this device
  262. primary_ip_choices = []
  263. interface_ips = IPAddress.objects.filter(interface__device=self.instance)
  264. primary_ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  265. nat_ips = IPAddress.objects.filter(nat_inside__interface__device=self.instance)\
  266. .select_related('nat_inside__interface')
  267. primary_ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
  268. self.fields['primary_ip'].choices = [(None, '---------')] + primary_ip_choices
  269. else:
  270. # An object that doesn't exist yet can't have any IPs assigned to it
  271. self.fields['primary_ip'].choices = []
  272. self.fields['primary_ip'].widget.attrs['readonly'] = True
  273. # Limit rack choices
  274. if self.is_bound:
  275. self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
  276. elif self.initial.get('site'):
  277. self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
  278. else:
  279. self.fields['rack'].choices = []
  280. # Rack position
  281. try:
  282. pk = self.instance.pk if self.instance.pk else None
  283. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  284. position_choices = Rack.objects.get(pk=self.data['rack'])\
  285. .get_rack_units(face=self.data.get('face'), exclude=pk)
  286. elif self.initial.get('rack') and str(self.initial.get('face')):
  287. position_choices = Rack.objects.get(pk=self.initial['rack'])\
  288. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  289. else:
  290. position_choices = []
  291. except Rack.DoesNotExist:
  292. position_choices = []
  293. self.fields['position'].choices = [('', '---------')] + [
  294. (p['id'], {
  295. 'label': p['name'],
  296. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  297. }) for p in position_choices
  298. ]
  299. # Limit device_type choices
  300. if self.is_bound:
  301. self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
  302. .select_related('manufacturer')
  303. elif self.initial.get('manufacturer'):
  304. self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
  305. .select_related('manufacturer')
  306. else:
  307. self.fields['device_type'].choices = []
  308. class DeviceFromCSVForm(forms.ModelForm):
  309. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
  310. error_messages={'invalid_choice': 'Invalid device role.'})
  311. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
  312. error_messages={'invalid_choice': 'Invalid manufacturer.'})
  313. model_name = forms.CharField()
  314. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
  315. error_messages={'invalid_choice': 'Invalid platform.'})
  316. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
  317. 'invalid_choice': 'Invalid site name.',
  318. })
  319. rack_name = forms.CharField()
  320. face = forms.CharField(required=False)
  321. class Meta:
  322. model = Device
  323. fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
  324. 'position', 'face']
  325. def clean(self):
  326. manufacturer = self.cleaned_data.get('manufacturer')
  327. model_name = self.cleaned_data.get('model_name')
  328. site = self.cleaned_data.get('site')
  329. rack_name = self.cleaned_data.get('rack_name')
  330. # Validate device type
  331. if manufacturer and model_name:
  332. try:
  333. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  334. except DeviceType.DoesNotExist:
  335. self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
  336. # Validate rack
  337. if site and rack_name:
  338. try:
  339. self.instance.rack = Rack.objects.get(site=site, name=rack_name)
  340. except Rack.DoesNotExist:
  341. self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
  342. def clean_face(self):
  343. face = self.cleaned_data['face']
  344. if face:
  345. try:
  346. return {
  347. 'front': 0,
  348. 'rear': 1,
  349. }[face.lower()]
  350. except KeyError:
  351. raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
  352. return face
  353. class DeviceImportForm(BulkImportForm, BootstrapMixin):
  354. csv = CSVDataField(csv_form=DeviceFromCSVForm)
  355. class DeviceBulkEditForm(forms.Form, BootstrapMixin):
  356. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  357. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
  358. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
  359. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
  360. platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
  361. status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
  362. serial = forms.CharField(max_length=50, required=False, label='Serial Number')
  363. class DeviceBulkDeleteForm(ConfirmationForm):
  364. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  365. def device_site_choices():
  366. site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
  367. return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
  368. def device_role_choices():
  369. role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
  370. return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
  371. def device_type_choices():
  372. type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
  373. return [(t.pk, '{} ({})'.format(t, t.device_count)) for t in type_choices]
  374. def device_platform_choices():
  375. platform_choices = Platform.objects.annotate(device_count=Count('devices'))
  376. return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
  377. class DeviceFilterForm(forms.Form, BootstrapMixin):
  378. site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
  379. widget=forms.SelectMultiple(attrs={'size': 8}))
  380. role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
  381. widget=forms.SelectMultiple(attrs={'size': 8}))
  382. device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
  383. widget=forms.SelectMultiple(attrs={'size': 8}))
  384. platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
  385. status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
  386. #
  387. # Console ports
  388. #
  389. class ConsolePortForm(forms.ModelForm, BootstrapMixin):
  390. class Meta:
  391. model = ConsolePort
  392. fields = ['device', 'name']
  393. widgets = {
  394. 'device': forms.HiddenInput(),
  395. }
  396. class ConsolePortCreateForm(forms.Form, BootstrapMixin):
  397. name_pattern = ExpandableNameField(label='Name')
  398. class ConsoleConnectionCSVForm(forms.Form):
  399. console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
  400. to_field_name='name',
  401. error_messages={'invalid_choice': 'Console server not found'})
  402. cs_port = forms.CharField()
  403. device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  404. error_messages={'invalid_choice': 'Device not found'})
  405. console_port = forms.CharField()
  406. status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
  407. def clean(self):
  408. # Validate console server port
  409. if self.cleaned_data.get('console_server'):
  410. try:
  411. cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
  412. name=self.cleaned_data['cs_port'])
  413. if ConsolePort.objects.filter(cs_port=cs_port):
  414. raise forms.ValidationError("Console server port is already occupied (by {} {})"
  415. .format(cs_port.connected_console.device, cs_port.connected_console))
  416. except ConsoleServerPort.DoesNotExist:
  417. raise forms.ValidationError("Invalid console server port ({} {})"
  418. .format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
  419. # Validate console port
  420. if self.cleaned_data.get('device'):
  421. try:
  422. console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
  423. name=self.cleaned_data['console_port'])
  424. if console_port.cs_port:
  425. raise forms.ValidationError("Console port is already connected (to {} {})"
  426. .format(console_port.cs_port.device, console_port.cs_port))
  427. except ConsolePort.DoesNotExist:
  428. raise forms.ValidationError("Invalid console port ({} {})"
  429. .format(self.cleaned_data['device'], self.cleaned_data['console_port']))
  430. class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
  431. csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
  432. def clean(self):
  433. records = self.cleaned_data.get('csv')
  434. if not records:
  435. return
  436. connection_list = []
  437. for i, record in enumerate(records, start=1):
  438. form = self.fields['csv'].csv_form(data=record)
  439. if form.is_valid():
  440. console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
  441. name=form.cleaned_data['console_port'])
  442. console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
  443. name=form.cleaned_data['cs_port'])
  444. if form.cleaned_data['status'] == 'planned':
  445. console_port.connection_status = CONNECTION_STATUS_PLANNED
  446. else:
  447. console_port.connection_status = CONNECTION_STATUS_CONNECTED
  448. connection_list.append(console_port)
  449. else:
  450. for field, errors in form.errors.items():
  451. for e in errors:
  452. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  453. self.cleaned_data['csv'] = connection_list
  454. class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
  455. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  456. widget=forms.Select(attrs={'filter-for': 'console_server'}))
  457. console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
  458. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
  459. attrs={'filter-for': 'cs_port'}))
  460. livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
  461. query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
  462. )
  463. cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
  464. widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
  465. disabled_indicator='connected_console'))
  466. class Meta:
  467. model = ConsolePort
  468. fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
  469. labels = {
  470. 'cs_port': 'Port',
  471. 'connection_status': 'Status',
  472. }
  473. def __init__(self, *args, **kwargs):
  474. super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
  475. if not self.instance.pk:
  476. raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
  477. self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
  478. self.fields['cs_port'].required = True
  479. self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
  480. # Initialize console server choices
  481. if self.is_bound and self.data.get('rack'):
  482. self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
  483. elif self.initial.get('rack'):
  484. self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
  485. else:
  486. self.fields['console_server'].choices = []
  487. # Initialize CS port choices
  488. if self.is_bound:
  489. self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.data['console_server'])
  490. elif self.initial.get('console_server', None):
  491. self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.initial['console_server'])
  492. else:
  493. self.fields['cs_port'].choices = []
  494. #
  495. # Console server ports
  496. #
  497. class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
  498. class Meta:
  499. model = ConsoleServerPort
  500. fields = ['device', 'name']
  501. widgets = {
  502. 'device': forms.HiddenInput(),
  503. }
  504. class ConsoleServerPortCreateForm(forms.Form, BootstrapMixin):
  505. name_pattern = ExpandableNameField(label='Name')
  506. class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
  507. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  508. widget=forms.Select(attrs={'filter-for': 'device'}))
  509. device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  510. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
  511. attrs={'filter-for': 'port'}))
  512. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  513. query_key='q', query_url='dcim-api:device_list', field_to_update='device')
  514. )
  515. port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
  516. widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
  517. disabled_indicator='cs_port'))
  518. connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
  519. widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
  520. class Meta:
  521. fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
  522. labels = {
  523. 'connection_status': 'Status',
  524. }
  525. def __init__(self, consoleserverport, *args, **kwargs):
  526. super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
  527. self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
  528. # Initialize device choices
  529. if self.is_bound and self.data.get('rack'):
  530. self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
  531. elif self.initial.get('rack', None):
  532. self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
  533. else:
  534. self.fields['device'].choices = []
  535. # Initialize port choices
  536. if self.is_bound:
  537. self.fields['port'].queryset = ConsolePort.objects.filter(device__pk=self.data['device'])
  538. elif self.initial.get('device', None):
  539. self.fields['port'].queryset = ConsolePort.objects.filter(device_pk=self.initial['device'])
  540. else:
  541. self.fields['port'].choices = []
  542. #
  543. # Power ports
  544. #
  545. class PowerPortForm(forms.ModelForm, BootstrapMixin):
  546. class Meta:
  547. model = PowerPort
  548. fields = ['device', 'name']
  549. widgets = {
  550. 'device': forms.HiddenInput(),
  551. }
  552. class PowerPortCreateForm(forms.Form, BootstrapMixin):
  553. name_pattern = ExpandableNameField(label='Name')
  554. class PowerConnectionCSVForm(forms.Form):
  555. pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
  556. error_messages={'invalid_choice': 'PDU not found.'})
  557. power_outlet = forms.CharField()
  558. device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  559. error_messages={'invalid_choice': 'Device not found'})
  560. power_port = forms.CharField()
  561. status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
  562. def clean(self):
  563. # Validate power outlet
  564. if self.cleaned_data.get('pdu'):
  565. try:
  566. power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
  567. name=self.cleaned_data['power_outlet'])
  568. if PowerPort.objects.filter(power_outlet=power_outlet):
  569. raise forms.ValidationError("Power outlet is already occupied (by {} {})"
  570. .format(power_outlet.connected_port.device,
  571. power_outlet.connected_port))
  572. except PowerOutlet.DoesNotExist:
  573. raise forms.ValidationError("Invalid PDU port ({} {})"
  574. .format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
  575. # Validate power port
  576. if self.cleaned_data.get('device'):
  577. try:
  578. power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
  579. name=self.cleaned_data['power_port'])
  580. if power_port.power_outlet:
  581. raise forms.ValidationError("Power port is already connected (to {} {})"
  582. .format(power_port.power_outlet.device, power_port.power_outlet))
  583. except PowerPort.DoesNotExist:
  584. raise forms.ValidationError("Invalid power port ({} {})"
  585. .format(self.cleaned_data['device'], self.cleaned_data['power_port']))
  586. class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
  587. csv = CSVDataField(csv_form=PowerConnectionCSVForm)
  588. def clean(self):
  589. records = self.cleaned_data.get('csv')
  590. if not records:
  591. return
  592. connection_list = []
  593. for i, record in enumerate(records, start=1):
  594. form = self.fields['csv'].csv_form(data=record)
  595. if form.is_valid():
  596. power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
  597. name=form.cleaned_data['power_port'])
  598. power_port.power_outlet = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
  599. name=form.cleaned_data['power_outlet'])
  600. if form.cleaned_data['status'] == 'planned':
  601. power_port.connection_status = CONNECTION_STATUS_PLANNED
  602. else:
  603. power_port.connection_status = CONNECTION_STATUS_CONNECTED
  604. connection_list.append(power_port)
  605. else:
  606. for field, errors in form.errors.items():
  607. for e in errors:
  608. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  609. self.cleaned_data['csv'] = connection_list
  610. class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
  611. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  612. widget=forms.Select(attrs={'filter-for': 'pdu'}))
  613. pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
  614. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
  615. attrs={'filter-for': 'power_outlet'}))
  616. livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
  617. query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
  618. )
  619. power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
  620. widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
  621. disabled_indicator='connected_port'))
  622. class Meta:
  623. model = PowerPort
  624. fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
  625. labels = {
  626. 'power_outlet': 'Outlet',
  627. 'connection_status': 'Status',
  628. }
  629. def __init__(self, *args, **kwargs):
  630. super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
  631. if not self.instance.pk:
  632. raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
  633. self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
  634. self.fields['power_outlet'].required = True
  635. self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
  636. # Initialize PDU choices
  637. if self.is_bound and self.data.get('rack'):
  638. self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
  639. elif self.initial.get('rack', None):
  640. self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
  641. else:
  642. self.fields['pdu'].choices = []
  643. # Initialize power outlet choices
  644. if self.is_bound:
  645. self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.data['pdu'])
  646. elif self.initial.get('pdu', None):
  647. self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.initial['pdu'])
  648. else:
  649. self.fields['power_outlet'].choices = []
  650. #
  651. # Power outlets
  652. #
  653. class PowerOutletForm(forms.ModelForm, BootstrapMixin):
  654. class Meta:
  655. model = PowerOutlet
  656. fields = ['device', 'name']
  657. widgets = {
  658. 'device': forms.HiddenInput(),
  659. }
  660. class PowerOutletCreateForm(forms.Form, BootstrapMixin):
  661. name_pattern = ExpandableNameField(label='Name')
  662. class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
  663. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  664. widget=forms.Select(attrs={'filter-for': 'device'}))
  665. device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  666. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
  667. attrs={'filter-for': 'port'}))
  668. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  669. query_key='q', query_url='dcim-api:device_list', field_to_update='device')
  670. )
  671. port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
  672. widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
  673. disabled_indicator='power_outlet'))
  674. connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
  675. widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
  676. class Meta:
  677. fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
  678. labels = {
  679. 'connection_status': 'Status',
  680. }
  681. def __init__(self, poweroutlet, *args, **kwargs):
  682. super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
  683. self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
  684. # Initialize device choices
  685. if self.is_bound and self.data.get('rack'):
  686. self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
  687. elif self.initial.get('rack', None):
  688. self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
  689. else:
  690. self.fields['device'].choices = []
  691. # Initialize port choices
  692. if self.is_bound:
  693. self.fields['port'].queryset = PowerPort.objects.filter(device__pk=self.data['device'])
  694. elif self.initial.get('device', None):
  695. self.fields['port'].queryset = PowerPort.objects.filter(device_pk=self.initial['device'])
  696. else:
  697. self.fields['port'].choices = []
  698. #
  699. # Interfaces
  700. #
  701. class InterfaceForm(forms.ModelForm, BootstrapMixin):
  702. class Meta:
  703. model = Interface
  704. fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
  705. widgets = {
  706. 'device': forms.HiddenInput(),
  707. }
  708. class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
  709. name_pattern = ExpandableNameField(label='Name')
  710. class Meta:
  711. model = Interface
  712. fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
  713. class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
  714. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  715. #
  716. # Interface connections
  717. #
  718. class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
  719. interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
  720. rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  721. widget=forms.Select(attrs={'filter-for': 'device_b'}))
  722. device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  723. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
  724. attrs={'filter-for': 'interface_b'}))
  725. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  726. query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
  727. )
  728. interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
  729. widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
  730. disabled_indicator='is_connected'))
  731. class Meta:
  732. model = InterfaceConnection
  733. fields = ['interface_a', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
  734. def __init__(self, device_a, *args, **kwargs):
  735. super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
  736. self.fields['rack_b'].queryset = Rack.objects.filter(site=device_a.rack.site)
  737. # Initialize interface A choices
  738. device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
  739. .select_related('circuit', 'connected_as_a', 'connected_as_b')
  740. self.fields['interface_a'].choices = [
  741. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
  742. ]
  743. # Initialize device_b choices if rack_b is set
  744. if self.is_bound and self.data.get('rack_b'):
  745. self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
  746. elif self.initial.get('rack_b'):
  747. self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
  748. else:
  749. self.fields['device_b'].choices = []
  750. # Initialize interface_b choices if device_b is set
  751. if self.is_bound:
  752. device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
  753. .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
  754. elif self.initial.get('device_b'):
  755. device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \
  756. .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
  757. else:
  758. device_b_interfaces = []
  759. self.fields['interface_b'].choices = [
  760. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
  761. ]
  762. class InterfaceConnectionCSVForm(forms.Form):
  763. device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  764. error_messages={'invalid_choice': 'Device A not found.'})
  765. interface_a = forms.CharField()
  766. device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  767. error_messages={'invalid_choice': 'Device B not found.'})
  768. interface_b = forms.CharField()
  769. status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
  770. def clean(self):
  771. # Validate interface A
  772. if self.cleaned_data.get('device_a'):
  773. try:
  774. interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
  775. name=self.cleaned_data['interface_a'])
  776. except Interface.DoesNotExist:
  777. raise forms.ValidationError("Invalid interface ({} {})"
  778. .format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
  779. try:
  780. InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
  781. raise forms.ValidationError("{} {} is already connected"
  782. .format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
  783. except InterfaceConnection.DoesNotExist:
  784. pass
  785. # Validate interface B
  786. if self.cleaned_data.get('device_b'):
  787. try:
  788. interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
  789. name=self.cleaned_data['interface_b'])
  790. except Interface.DoesNotExist:
  791. raise forms.ValidationError("Invalid interface ({} {})"
  792. .format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
  793. try:
  794. InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
  795. raise forms.ValidationError("{} {} is already connected"
  796. .format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
  797. except InterfaceConnection.DoesNotExist:
  798. pass
  799. class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
  800. csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
  801. def clean(self):
  802. records = self.cleaned_data.get('csv')
  803. if not records:
  804. return
  805. connection_list = []
  806. occupied_interfaces = []
  807. for i, record in enumerate(records, start=1):
  808. form = self.fields['csv'].csv_form(data=record)
  809. if form.is_valid():
  810. interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
  811. name=form.cleaned_data['interface_a'])
  812. if interface_a in occupied_interfaces:
  813. raise forms.ValidationError("{} {} found in multiple connections"
  814. .format(interface_a.device.name, interface_a.name))
  815. interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
  816. name=form.cleaned_data['interface_b'])
  817. if interface_b in occupied_interfaces:
  818. raise forms.ValidationError("{} {} found in multiple connections"
  819. .format(interface_b.device.name, interface_b.name))
  820. connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
  821. if form.cleaned_data['status'] == 'planned':
  822. connection.connection_status = CONNECTION_STATUS_PLANNED
  823. else:
  824. connection.connection_status = CONNECTION_STATUS_CONNECTED
  825. connection_list.append(connection)
  826. occupied_interfaces.append(interface_a)
  827. occupied_interfaces.append(interface_b)
  828. else:
  829. for field, errors in form.errors.items():
  830. for e in errors:
  831. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  832. self.cleaned_data['csv'] = connection_list
  833. class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
  834. confirm = forms.BooleanField(required=True)
  835. # Used for HTTP redirect upon successful deletion
  836. device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
  837. #
  838. # Device bays
  839. #
  840. class DeviceBayForm(forms.ModelForm, BootstrapMixin):
  841. class Meta:
  842. model = DeviceBay
  843. fields = ['device', 'name']
  844. widgets = {
  845. 'device': forms.HiddenInput(),
  846. }
  847. class DeviceBayCreateForm(forms.Form, BootstrapMixin):
  848. name_pattern = ExpandableNameField(label='Name')
  849. class PopulateDeviceBayForm(forms.Form, BootstrapMixin):
  850. installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
  851. help_text="Child devices must first be created within the rack occupied "
  852. "by the parent device. Then they can be assigned to a bay.")
  853. def __init__(self, device_bay, *args, **kwargs):
  854. super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
  855. children_queryset = Device.objects.filter(rack=device_bay.device.rack,
  856. parent_bay__isnull=True,
  857. device_type__u_height=0,
  858. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\
  859. .exclude(pk=device_bay.device.pk)
  860. self.fields['installed_device'].queryset = children_queryset
  861. #
  862. # Connections
  863. #
  864. class ConsoleConnectionFilterForm(forms.Form, BootstrapMixin):
  865. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  866. class PowerConnectionFilterForm(forms.Form, BootstrapMixin):
  867. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  868. class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
  869. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  870. #
  871. # IP addresses
  872. #
  873. class IPAddressForm(forms.ModelForm, BootstrapMixin):
  874. set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
  875. class Meta:
  876. model = IPAddress
  877. fields = ['address', 'vrf', 'interface', 'set_as_primary']
  878. help_texts = {
  879. 'address': 'IPv4 or IPv6 address (with mask)'
  880. }
  881. def __init__(self, device, *args, **kwargs):
  882. super(IPAddressForm, self).__init__(*args, **kwargs)
  883. self.fields['vrf'].empty_label = 'Global'
  884. self.fields['interface'].queryset = device.interfaces.all()
  885. self.fields['interface'].required = True
  886. # If this device does not have any IP addresses assigned, default to setting the first IP as its primary
  887. if not IPAddress.objects.filter(interface__device=device).count():
  888. self.fields['set_as_primary'].initial = True
  889. #
  890. # Interfaces
  891. #
  892. class ModuleForm(forms.ModelForm, BootstrapMixin):
  893. class Meta:
  894. model = Module
  895. fields = ['name', 'part_id', 'serial']