forms.py 57 KB

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