forms.py 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778
  1. from __future__ import unicode_literals
  2. from mptt.forms import TreeNodeChoiceField
  3. import re
  4. from django import forms
  5. from django.contrib.postgres.forms.array import SimpleArrayField
  6. from django.db.models import Count, Q
  7. from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
  8. from ipam.models import IPAddress
  9. from tenancy.forms import TenancyForm
  10. from tenancy.models import Tenant
  11. from utilities.forms import (
  12. APISelect, ArrayFieldSelectMultiple, add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
  13. ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
  14. FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
  15. FilterTreeNodeMultipleChoiceField,
  16. )
  17. from .formfields import MACAddressFormField
  18. from .models import (
  19. DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
  20. ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
  21. IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
  22. InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
  23. RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
  24. Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
  25. )
  26. DEVICE_BY_PK_RE = '{\d+\}'
  27. def get_device_by_name_or_pk(name):
  28. """
  29. Attempt to retrieve a device by either its name or primary key ('{pk}').
  30. """
  31. if re.match(DEVICE_BY_PK_RE, name):
  32. pk = name.strip('{}')
  33. device = Device.objects.get(pk=pk)
  34. else:
  35. device = Device.objects.get(name=name)
  36. return device
  37. class DeviceComponentForm(BootstrapMixin, forms.Form):
  38. """
  39. Allow inclusion of the parent device as context for limiting field choices.
  40. """
  41. def __init__(self, device, *args, **kwargs):
  42. self.device = device
  43. super(DeviceComponentForm, self).__init__(*args, **kwargs)
  44. #
  45. # Regions
  46. #
  47. class RegionForm(BootstrapMixin, forms.ModelForm):
  48. slug = SlugField()
  49. class Meta:
  50. model = Region
  51. fields = ['parent', 'name', 'slug']
  52. #
  53. # Sites
  54. #
  55. class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  56. region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
  57. slug = SlugField()
  58. comments = CommentField()
  59. class Meta:
  60. model = Site
  61. fields = [
  62. 'name', 'slug', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'physical_address',
  63. 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
  64. ]
  65. widgets = {
  66. 'physical_address': SmallTextarea(attrs={'rows': 3}),
  67. 'shipping_address': SmallTextarea(attrs={'rows': 3}),
  68. }
  69. help_texts = {
  70. 'name': "Full name of the site",
  71. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  72. 'asn': "BGP autonomous system number",
  73. 'physical_address': "Physical location of the building (e.g. for GPS)",
  74. 'shipping_address': "If different from the physical address"
  75. }
  76. class SiteCSVForm(forms.ModelForm):
  77. region = forms.ModelChoiceField(
  78. queryset=Region.objects.all(),
  79. required=False,
  80. to_field_name='name',
  81. help_text='Name of assigned region',
  82. error_messages={
  83. 'invalid_choice': 'Region not found.',
  84. }
  85. )
  86. tenant = forms.ModelChoiceField(
  87. queryset=Tenant.objects.all(),
  88. required=False,
  89. to_field_name='name',
  90. help_text='Name of assigned tenant',
  91. error_messages={
  92. 'invalid_choice': 'Tenant not found.',
  93. }
  94. )
  95. class Meta:
  96. model = Site
  97. fields = [
  98. 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
  99. 'contact_name', 'contact_phone', 'contact_email', 'comments',
  100. ]
  101. help_texts = {
  102. 'name': 'Site name',
  103. 'slug': 'URL-friendly slug',
  104. 'asn': '32-bit autonomous system number',
  105. }
  106. class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  107. pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
  108. region = TreeNodeChoiceField(queryset=Region.objects.all(), required=False)
  109. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  110. asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
  111. class Meta:
  112. nullable_fields = ['region', 'tenant', 'asn']
  113. class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
  114. model = Site
  115. q = forms.CharField(required=False, label='Search')
  116. region = FilterTreeNodeMultipleChoiceField(
  117. queryset=Region.objects.annotate(filter_count=Count('sites')),
  118. to_field_name='slug',
  119. required=False,
  120. )
  121. tenant = FilterChoiceField(
  122. queryset=Tenant.objects.annotate(filter_count=Count('sites')),
  123. to_field_name='slug',
  124. null_option=(0, 'None')
  125. )
  126. #
  127. # Rack groups
  128. #
  129. class RackGroupForm(BootstrapMixin, forms.ModelForm):
  130. slug = SlugField()
  131. class Meta:
  132. model = RackGroup
  133. fields = ['site', 'name', 'slug']
  134. class RackGroupFilterForm(BootstrapMixin, forms.Form):
  135. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
  136. #
  137. # Rack roles
  138. #
  139. class RackRoleForm(BootstrapMixin, forms.ModelForm):
  140. slug = SlugField()
  141. class Meta:
  142. model = RackRole
  143. fields = ['name', 'slug', 'color']
  144. #
  145. # Racks
  146. #
  147. class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  148. group = ChainedModelChoiceField(
  149. queryset=RackGroup.objects.all(),
  150. chains=(
  151. ('site', 'site'),
  152. ),
  153. required=False,
  154. widget=APISelect(
  155. api_url='/api/dcim/rack-groups/?site_id={{site}}',
  156. )
  157. )
  158. comments = CommentField()
  159. class Meta:
  160. model = Rack
  161. fields = [
  162. 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'role', 'type', 'width', 'u_height',
  163. 'desc_units', 'comments',
  164. ]
  165. help_texts = {
  166. 'site': "The site at which the rack exists",
  167. 'name': "Organizational rack name",
  168. 'facility_id': "The unique rack ID assigned by the facility",
  169. 'u_height': "Height in rack units",
  170. }
  171. widgets = {
  172. 'site': forms.Select(attrs={'filter-for': 'group'}),
  173. }
  174. class RackCSVForm(forms.ModelForm):
  175. site = forms.ModelChoiceField(
  176. queryset=Site.objects.all(),
  177. to_field_name='name',
  178. help_text='Name of parent site',
  179. error_messages={
  180. 'invalid_choice': 'Site not found.',
  181. }
  182. )
  183. group_name = forms.CharField(
  184. help_text='Name of rack group',
  185. required=False
  186. )
  187. tenant = forms.ModelChoiceField(
  188. queryset=Tenant.objects.all(),
  189. required=False,
  190. to_field_name='name',
  191. help_text='Name of assigned tenant',
  192. error_messages={
  193. 'invalid_choice': 'Tenant not found.',
  194. }
  195. )
  196. role = forms.ModelChoiceField(
  197. queryset=RackRole.objects.all(),
  198. required=False,
  199. to_field_name='name',
  200. help_text='Name of assigned role',
  201. error_messages={
  202. 'invalid_choice': 'Role not found.',
  203. }
  204. )
  205. type = CSVChoiceField(
  206. choices=RACK_TYPE_CHOICES,
  207. required=False,
  208. help_text='Rack type'
  209. )
  210. width = forms.ChoiceField(
  211. choices=(
  212. (RACK_WIDTH_19IN, '19'),
  213. (RACK_WIDTH_23IN, '23'),
  214. ),
  215. help_text='Rail-to-rail width (in inches)'
  216. )
  217. class Meta:
  218. model = Rack
  219. fields = [
  220. 'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
  221. ]
  222. help_texts = {
  223. 'name': 'Rack name',
  224. 'u_height': 'Height in rack units',
  225. }
  226. def clean(self):
  227. super(RackCSVForm, self).clean()
  228. site = self.cleaned_data.get('site')
  229. group_name = self.cleaned_data.get('group_name')
  230. # Validate rack group
  231. if group_name:
  232. try:
  233. self.instance.group = RackGroup.objects.get(site=site, name=group_name)
  234. except RackGroup.DoesNotExist:
  235. raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
  236. class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  237. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  238. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
  239. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
  240. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  241. role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
  242. type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
  243. width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
  244. u_height = forms.IntegerField(required=False, label='Height (U)')
  245. desc_units = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Descending units')
  246. comments = CommentField(widget=SmallTextarea)
  247. class Meta:
  248. nullable_fields = ['group', 'tenant', 'role', 'comments']
  249. class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
  250. model = Rack
  251. q = forms.CharField(required=False, label='Search')
  252. site = FilterChoiceField(
  253. queryset=Site.objects.annotate(filter_count=Count('racks')),
  254. to_field_name='slug'
  255. )
  256. group_id = FilterChoiceField(
  257. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
  258. label='Rack group',
  259. null_option=(0, 'None')
  260. )
  261. tenant = FilterChoiceField(
  262. queryset=Tenant.objects.annotate(filter_count=Count('racks')),
  263. to_field_name='slug',
  264. null_option=(0, 'None')
  265. )
  266. role = FilterChoiceField(
  267. queryset=RackRole.objects.annotate(filter_count=Count('racks')),
  268. to_field_name='slug',
  269. null_option=(0, 'None')
  270. )
  271. #
  272. # Rack reservations
  273. #
  274. class RackReservationForm(BootstrapMixin, forms.ModelForm):
  275. units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
  276. class Meta:
  277. model = RackReservation
  278. fields = ['units', 'description']
  279. def __init__(self, *args, **kwargs):
  280. super(RackReservationForm, self).__init__(*args, **kwargs)
  281. # Populate rack unit choices
  282. self.fields['units'].widget.choices = self._get_unit_choices()
  283. def _get_unit_choices(self):
  284. rack = self.instance.rack
  285. reserved_units = []
  286. for resv in rack.reservations.exclude(pk=self.instance.pk):
  287. for u in resv.units:
  288. reserved_units.append(u)
  289. unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
  290. return unit_choices
  291. class RackReservationFilterForm(BootstrapMixin, forms.Form):
  292. q = forms.CharField(required=False, label='Search')
  293. site = FilterChoiceField(
  294. queryset=Site.objects.annotate(filter_count=Count('racks__reservations')),
  295. to_field_name='slug'
  296. )
  297. group_id = FilterChoiceField(
  298. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
  299. label='Rack group',
  300. null_option=(0, 'None')
  301. )
  302. #
  303. # Manufacturers
  304. #
  305. class ManufacturerForm(BootstrapMixin, forms.ModelForm):
  306. slug = SlugField()
  307. class Meta:
  308. model = Manufacturer
  309. fields = ['name', 'slug']
  310. #
  311. # Device types
  312. #
  313. class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
  314. slug = SlugField(slug_source='model')
  315. class Meta:
  316. model = DeviceType
  317. fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
  318. 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
  319. labels = {
  320. 'interface_ordering': 'Order interfaces by',
  321. }
  322. class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  323. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  324. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
  325. u_height = forms.IntegerField(min_value=1, required=False)
  326. is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
  327. interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
  328. is_console_server = forms.NullBooleanField(
  329. required=False, widget=BulkEditNullBooleanSelect, label='Is a console server'
  330. )
  331. is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
  332. is_network_device = forms.NullBooleanField(
  333. required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
  334. )
  335. class Meta:
  336. nullable_fields = []
  337. class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
  338. model = DeviceType
  339. q = forms.CharField(required=False, label='Search')
  340. manufacturer = FilterChoiceField(
  341. queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
  342. to_field_name='slug'
  343. )
  344. is_console_server = forms.BooleanField(
  345. required=False, label='Is a console server', widget=forms.CheckboxInput(attrs={'value': 'True'}))
  346. is_pdu = forms.BooleanField(
  347. required=False, label='Is a PDU', widget=forms.CheckboxInput(attrs={'value': 'True'})
  348. )
  349. is_network_device = forms.BooleanField(
  350. required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
  351. )
  352. subdevice_role = forms.NullBooleanField(
  353. required=False, label='Subdevice role', widget=forms.Select(choices=(
  354. ('', '---------'),
  355. (SUBDEVICE_ROLE_PARENT, 'Parent'),
  356. (SUBDEVICE_ROLE_CHILD, 'Child'),
  357. ))
  358. )
  359. #
  360. # Device component templates
  361. #
  362. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  363. class Meta:
  364. model = ConsolePortTemplate
  365. fields = ['device_type', 'name']
  366. widgets = {
  367. 'device_type': forms.HiddenInput(),
  368. }
  369. class ConsolePortTemplateCreateForm(DeviceComponentForm):
  370. name_pattern = ExpandableNameField(label='Name')
  371. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  372. class Meta:
  373. model = ConsoleServerPortTemplate
  374. fields = ['device_type', 'name']
  375. widgets = {
  376. 'device_type': forms.HiddenInput(),
  377. }
  378. class ConsoleServerPortTemplateCreateForm(DeviceComponentForm):
  379. name_pattern = ExpandableNameField(label='Name')
  380. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  381. class Meta:
  382. model = PowerPortTemplate
  383. fields = ['device_type', 'name']
  384. widgets = {
  385. 'device_type': forms.HiddenInput(),
  386. }
  387. class PowerPortTemplateCreateForm(DeviceComponentForm):
  388. name_pattern = ExpandableNameField(label='Name')
  389. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  390. class Meta:
  391. model = PowerOutletTemplate
  392. fields = ['device_type', 'name']
  393. widgets = {
  394. 'device_type': forms.HiddenInput(),
  395. }
  396. class PowerOutletTemplateCreateForm(DeviceComponentForm):
  397. name_pattern = ExpandableNameField(label='Name')
  398. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  399. class Meta:
  400. model = InterfaceTemplate
  401. fields = ['device_type', 'name', 'form_factor', 'mgmt_only']
  402. widgets = {
  403. 'device_type': forms.HiddenInput(),
  404. }
  405. class InterfaceTemplateCreateForm(DeviceComponentForm):
  406. name_pattern = ExpandableNameField(label='Name')
  407. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  408. mgmt_only = forms.BooleanField(required=False, label='OOB Management')
  409. class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  410. pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
  411. form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
  412. mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
  413. class Meta:
  414. nullable_fields = []
  415. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  416. class Meta:
  417. model = DeviceBayTemplate
  418. fields = ['device_type', 'name']
  419. widgets = {
  420. 'device_type': forms.HiddenInput(),
  421. }
  422. class DeviceBayTemplateCreateForm(DeviceComponentForm):
  423. name_pattern = ExpandableNameField(label='Name')
  424. #
  425. # Device roles
  426. #
  427. class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
  428. slug = SlugField()
  429. class Meta:
  430. model = DeviceRole
  431. fields = ['name', 'slug', 'color']
  432. #
  433. # Platforms
  434. #
  435. class PlatformForm(BootstrapMixin, forms.ModelForm):
  436. slug = SlugField()
  437. class Meta:
  438. model = Platform
  439. fields = ['name', 'slug', 'napalm_driver', 'rpc_client']
  440. #
  441. # Devices
  442. #
  443. class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  444. site = forms.ModelChoiceField(
  445. queryset=Site.objects.all(),
  446. widget=forms.Select(
  447. attrs={'filter-for': 'rack'}
  448. )
  449. )
  450. rack = ChainedModelChoiceField(
  451. queryset=Rack.objects.all(),
  452. chains=(
  453. ('site', 'site'),
  454. ),
  455. required=False,
  456. widget=APISelect(
  457. api_url='/api/dcim/racks/?site_id={{site}}',
  458. display_field='display_name',
  459. attrs={'filter-for': 'position'}
  460. )
  461. )
  462. position = forms.TypedChoiceField(
  463. required=False,
  464. empty_value=None,
  465. help_text="The lowest-numbered unit occupied by the device",
  466. widget=APISelect(
  467. api_url='/api/dcim/racks/{{rack}}/units/?face={{face}}',
  468. disabled_indicator='device'
  469. )
  470. )
  471. manufacturer = forms.ModelChoiceField(
  472. queryset=Manufacturer.objects.all(),
  473. widget=forms.Select(
  474. attrs={'filter-for': 'device_type'}
  475. )
  476. )
  477. device_type = ChainedModelChoiceField(
  478. queryset=DeviceType.objects.all(),
  479. chains=(
  480. ('manufacturer', 'manufacturer'),
  481. ),
  482. label='Device type',
  483. widget=APISelect(
  484. api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
  485. display_field='model'
  486. )
  487. )
  488. comments = CommentField()
  489. class Meta:
  490. model = Device
  491. fields = [
  492. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status',
  493. 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments',
  494. ]
  495. help_texts = {
  496. 'device_role': "The function this device serves",
  497. 'serial': "Chassis serial number",
  498. }
  499. widgets = {
  500. 'face': forms.Select(attrs={'filter-for': 'position'}),
  501. }
  502. def __init__(self, *args, **kwargs):
  503. # Initialize helper selectors
  504. instance = kwargs.get('instance')
  505. # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
  506. if instance and hasattr(instance, 'device_type'):
  507. initial = kwargs.get('initial', {}).copy()
  508. initial['manufacturer'] = instance.device_type.manufacturer
  509. kwargs['initial'] = initial
  510. super(DeviceForm, self).__init__(*args, **kwargs)
  511. if self.instance.pk:
  512. # Compile list of choices for primary IPv4 and IPv6 addresses
  513. for family in [4, 6]:
  514. ip_choices = [(None, '---------')]
  515. # Collect interface IPs
  516. interface_ips = IPAddress.objects.select_related('interface').filter(
  517. family=family, interface__device=self.instance
  518. )
  519. if interface_ips:
  520. ip_choices.append(
  521. ('Interface IPs', [
  522. (ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
  523. ])
  524. )
  525. # Collect NAT IPs
  526. nat_ips = IPAddress.objects.select_related('nat_inside').filter(
  527. family=family, nat_inside__interface__device=self.instance
  528. )
  529. if nat_ips:
  530. ip_choices.append(
  531. ('NAT IPs', [
  532. (ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
  533. ])
  534. )
  535. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  536. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  537. # can be flipped from one face to another.
  538. self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk)
  539. else:
  540. # An object that doesn't exist yet can't have any IPs assigned to it
  541. self.fields['primary_ip4'].choices = []
  542. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  543. self.fields['primary_ip6'].choices = []
  544. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  545. # Rack position
  546. pk = self.instance.pk if self.instance.pk else None
  547. try:
  548. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  549. position_choices = Rack.objects.get(pk=self.data['rack'])\
  550. .get_rack_units(face=self.data.get('face'), exclude=pk)
  551. elif self.initial.get('rack') and str(self.initial.get('face')):
  552. position_choices = Rack.objects.get(pk=self.initial['rack'])\
  553. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  554. else:
  555. position_choices = []
  556. except Rack.DoesNotExist:
  557. position_choices = []
  558. self.fields['position'].choices = [('', '---------')] + [
  559. (p['id'], {
  560. 'label': p['name'],
  561. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  562. }) for p in position_choices
  563. ]
  564. # Disable rack assignment if this is a child device installed in a parent device
  565. if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  566. self.fields['site'].disabled = True
  567. self.fields['rack'].disabled = True
  568. self.initial['site'] = self.instance.parent_bay.device.site_id
  569. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  570. class BaseDeviceCSVForm(forms.ModelForm):
  571. device_role = forms.ModelChoiceField(
  572. queryset=DeviceRole.objects.all(),
  573. to_field_name='name',
  574. help_text='Name of assigned role',
  575. error_messages={
  576. 'invalid_choice': 'Invalid device role.',
  577. }
  578. )
  579. tenant = forms.ModelChoiceField(
  580. queryset=Tenant.objects.all(),
  581. required=False,
  582. to_field_name='name',
  583. help_text='Name of assigned tenant',
  584. error_messages={
  585. 'invalid_choice': 'Tenant not found.',
  586. }
  587. )
  588. manufacturer = forms.ModelChoiceField(
  589. queryset=Manufacturer.objects.all(),
  590. to_field_name='name',
  591. help_text='Device type manufacturer',
  592. error_messages={
  593. 'invalid_choice': 'Invalid manufacturer.',
  594. }
  595. )
  596. model_name = forms.CharField(
  597. help_text='Device type model name'
  598. )
  599. platform = forms.ModelChoiceField(
  600. queryset=Platform.objects.all(),
  601. required=False,
  602. to_field_name='name',
  603. help_text='Name of assigned platform',
  604. error_messages={
  605. 'invalid_choice': 'Invalid platform.',
  606. }
  607. )
  608. status = CSVChoiceField(
  609. choices=STATUS_CHOICES,
  610. help_text='Operational status of device'
  611. )
  612. class Meta:
  613. fields = []
  614. model = Device
  615. help_texts = {
  616. 'name': 'Device name',
  617. }
  618. def clean(self):
  619. super(BaseDeviceCSVForm, self).clean()
  620. manufacturer = self.cleaned_data.get('manufacturer')
  621. model_name = self.cleaned_data.get('model_name')
  622. # Validate device type
  623. if manufacturer and model_name:
  624. try:
  625. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  626. except DeviceType.DoesNotExist:
  627. raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
  628. class DeviceCSVForm(BaseDeviceCSVForm):
  629. site = forms.ModelChoiceField(
  630. queryset=Site.objects.all(),
  631. to_field_name='name',
  632. help_text='Name of parent site',
  633. error_messages={
  634. 'invalid_choice': 'Invalid site name.',
  635. }
  636. )
  637. rack_group = forms.CharField(
  638. required=False,
  639. help_text='Parent rack\'s group (if any)'
  640. )
  641. rack_name = forms.CharField(
  642. required=False,
  643. help_text='Name of parent rack'
  644. )
  645. face = CSVChoiceField(
  646. choices=RACK_FACE_CHOICES,
  647. required=False,
  648. help_text='Mounted rack face'
  649. )
  650. class Meta(BaseDeviceCSVForm.Meta):
  651. fields = [
  652. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  653. 'site', 'rack_group', 'rack_name', 'position', 'face',
  654. ]
  655. def clean(self):
  656. super(DeviceCSVForm, self).clean()
  657. site = self.cleaned_data.get('site')
  658. rack_group = self.cleaned_data.get('rack_group')
  659. rack_name = self.cleaned_data.get('rack_name')
  660. # Validate rack
  661. if site and rack_group and rack_name:
  662. try:
  663. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  664. except Rack.DoesNotExist:
  665. raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
  666. elif site and rack_name:
  667. try:
  668. self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
  669. except Rack.DoesNotExist:
  670. raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
  671. class ChildDeviceCSVForm(BaseDeviceCSVForm):
  672. parent = FlexibleModelChoiceField(
  673. queryset=Device.objects.all(),
  674. to_field_name='name',
  675. help_text='Name or ID of parent device',
  676. error_messages={
  677. 'invalid_choice': 'Parent device not found.',
  678. }
  679. )
  680. device_bay_name = forms.CharField(
  681. help_text='Name of device bay',
  682. )
  683. class Meta(BaseDeviceCSVForm.Meta):
  684. fields = [
  685. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  686. 'parent', 'device_bay_name',
  687. ]
  688. def clean(self):
  689. super(ChildDeviceCSVForm, self).clean()
  690. parent = self.cleaned_data.get('parent')
  691. device_bay_name = self.cleaned_data.get('device_bay_name')
  692. # Validate device bay
  693. if parent and device_bay_name:
  694. try:
  695. self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
  696. # Inherit site and rack from parent device
  697. self.instance.site = parent.site
  698. self.instance.rack = parent.rack
  699. except DeviceBay.DoesNotExist:
  700. raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
  701. class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  702. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  703. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
  704. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
  705. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  706. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
  707. status = forms.ChoiceField(choices=add_blank_choice(STATUS_CHOICES), required=False, initial='')
  708. serial = forms.CharField(max_length=50, required=False, label='Serial Number')
  709. class Meta:
  710. nullable_fields = ['tenant', 'platform']
  711. def device_status_choices():
  712. status_counts = {}
  713. for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
  714. status_counts[status['status']] = status['count']
  715. return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES]
  716. class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  717. model = Device
  718. q = forms.CharField(required=False, label='Search')
  719. site = FilterChoiceField(
  720. queryset=Site.objects.annotate(filter_count=Count('devices')),
  721. to_field_name='slug',
  722. )
  723. rack_group_id = FilterChoiceField(
  724. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
  725. label='Rack group',
  726. )
  727. rack_id = FilterChoiceField(
  728. queryset=Rack.objects.annotate(filter_count=Count('devices')),
  729. label='Rack',
  730. null_option=(0, 'None'),
  731. )
  732. role = FilterChoiceField(
  733. queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
  734. to_field_name='slug',
  735. )
  736. tenant = FilterChoiceField(
  737. queryset=Tenant.objects.annotate(filter_count=Count('devices')),
  738. to_field_name='slug',
  739. null_option=(0, 'None'),
  740. )
  741. manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
  742. device_type_id = FilterChoiceField(
  743. queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
  744. filter_count=Count('instances'),
  745. ),
  746. label='Model',
  747. )
  748. platform = FilterChoiceField(
  749. queryset=Platform.objects.annotate(filter_count=Count('devices')),
  750. to_field_name='slug',
  751. null_option=(0, 'None'),
  752. )
  753. status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
  754. mac_address = forms.CharField(required=False, label='MAC address')
  755. #
  756. # Bulk device component creation
  757. #
  758. class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
  759. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  760. name_pattern = ExpandableNameField(label='Name')
  761. class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
  762. class Meta:
  763. model = Interface
  764. fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description']
  765. #
  766. # Console ports
  767. #
  768. class ConsolePortForm(BootstrapMixin, forms.ModelForm):
  769. class Meta:
  770. model = ConsolePort
  771. fields = ['device', 'name']
  772. widgets = {
  773. 'device': forms.HiddenInput(),
  774. }
  775. class ConsolePortCreateForm(DeviceComponentForm):
  776. name_pattern = ExpandableNameField(label='Name')
  777. class ConsoleConnectionCSVForm(forms.ModelForm):
  778. console_server = FlexibleModelChoiceField(
  779. queryset=Device.objects.filter(device_type__is_console_server=True),
  780. to_field_name='name',
  781. help_text='Console server name or ID',
  782. error_messages={
  783. 'invalid_choice': 'Console server not found',
  784. }
  785. )
  786. cs_port = forms.CharField(
  787. help_text='Console server port name'
  788. )
  789. device = FlexibleModelChoiceField(
  790. queryset=Device.objects.all(),
  791. to_field_name='name',
  792. help_text='Device name or ID',
  793. error_messages={
  794. 'invalid_choice': 'Device not found',
  795. }
  796. )
  797. console_port = forms.CharField(
  798. help_text='Console port name'
  799. )
  800. connection_status = CSVChoiceField(
  801. choices=CONNECTION_STATUS_CHOICES,
  802. help_text='Connection status'
  803. )
  804. class Meta:
  805. model = ConsolePort
  806. fields = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
  807. def clean_console_port(self):
  808. console_port_name = self.cleaned_data.get('console_port')
  809. if not self.cleaned_data.get('device') or not console_port_name:
  810. return None
  811. try:
  812. # Retrieve console port by name
  813. consoleport = ConsolePort.objects.get(
  814. device=self.cleaned_data['device'], name=console_port_name
  815. )
  816. # Check if the console port is already connected
  817. if consoleport.cs_port is not None:
  818. raise forms.ValidationError("{} {} is already connected".format(
  819. self.cleaned_data['device'], console_port_name
  820. ))
  821. except ConsolePort.DoesNotExist:
  822. raise forms.ValidationError("Invalid console port ({} {})".format(
  823. self.cleaned_data['device'], console_port_name
  824. ))
  825. self.instance = consoleport
  826. return consoleport
  827. def clean_cs_port(self):
  828. cs_port_name = self.cleaned_data.get('cs_port')
  829. if not self.cleaned_data.get('console_server') or not cs_port_name:
  830. return None
  831. try:
  832. # Retrieve console server port by name
  833. cs_port = ConsoleServerPort.objects.get(
  834. device=self.cleaned_data['console_server'], name=cs_port_name
  835. )
  836. # Check if the console server port is already connected
  837. if ConsolePort.objects.filter(cs_port=cs_port).count():
  838. raise forms.ValidationError("{} {} is already connected".format(
  839. self.cleaned_data['console_server'], cs_port_name
  840. ))
  841. except ConsoleServerPort.DoesNotExist:
  842. raise forms.ValidationError("Invalid console server port ({} {})".format(
  843. self.cleaned_data['console_server'], cs_port_name
  844. ))
  845. return cs_port
  846. class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  847. site = forms.ModelChoiceField(
  848. queryset=Site.objects.all(),
  849. required=False,
  850. widget=forms.Select(
  851. attrs={'filter-for': 'rack'}
  852. )
  853. )
  854. rack = ChainedModelChoiceField(
  855. queryset=Rack.objects.all(),
  856. chains=(
  857. ('site', 'site'),
  858. ),
  859. label='Rack',
  860. required=False,
  861. widget=APISelect(
  862. api_url='/api/dcim/racks/?site_id={{site}}',
  863. attrs={'filter-for': 'console_server', 'nullable': 'true'}
  864. )
  865. )
  866. console_server = ChainedModelChoiceField(
  867. queryset=Device.objects.filter(device_type__is_console_server=True),
  868. chains=(
  869. ('site', 'site'),
  870. ('rack', 'rack'),
  871. ),
  872. label='Console Server',
  873. required=False,
  874. widget=APISelect(
  875. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_console_server=True',
  876. display_field='display_name',
  877. attrs={'filter-for': 'cs_port'}
  878. )
  879. )
  880. livesearch = forms.CharField(
  881. required=False,
  882. label='Console Server',
  883. widget=Livesearch(
  884. query_key='q',
  885. query_url='dcim-api:device-list',
  886. field_to_update='console_server',
  887. )
  888. )
  889. cs_port = ChainedModelChoiceField(
  890. queryset=ConsoleServerPort.objects.all(),
  891. chains=(
  892. ('device', 'console_server'),
  893. ),
  894. label='Port',
  895. widget=APISelect(
  896. api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
  897. disabled_indicator='connected_console',
  898. )
  899. )
  900. class Meta:
  901. model = ConsolePort
  902. fields = ['site', 'rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
  903. labels = {
  904. 'cs_port': 'Port',
  905. 'connection_status': 'Status',
  906. }
  907. def __init__(self, *args, **kwargs):
  908. super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
  909. if not self.instance.pk:
  910. raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
  911. #
  912. # Console server ports
  913. #
  914. class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
  915. class Meta:
  916. model = ConsoleServerPort
  917. fields = ['device', 'name']
  918. widgets = {
  919. 'device': forms.HiddenInput(),
  920. }
  921. class ConsoleServerPortCreateForm(DeviceComponentForm):
  922. name_pattern = ExpandableNameField(label='Name')
  923. class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  924. site = forms.ModelChoiceField(
  925. queryset=Site.objects.all(),
  926. required=False,
  927. widget=forms.Select(
  928. attrs={'filter-for': 'rack'}
  929. )
  930. )
  931. rack = ChainedModelChoiceField(
  932. queryset=Rack.objects.all(),
  933. chains=(
  934. ('site', 'site'),
  935. ),
  936. label='Rack',
  937. required=False,
  938. widget=APISelect(
  939. api_url='/api/dcim/racks/?site_id={{site}}',
  940. attrs={'filter-for': 'device', 'nullable': 'true'}
  941. )
  942. )
  943. device = ChainedModelChoiceField(
  944. queryset=Device.objects.all(),
  945. chains=(
  946. ('site', 'site'),
  947. ('rack', 'rack'),
  948. ),
  949. label='Device',
  950. required=False,
  951. widget=APISelect(
  952. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
  953. display_field='display_name',
  954. attrs={'filter-for': 'port'}
  955. )
  956. )
  957. livesearch = forms.CharField(
  958. required=False,
  959. label='Device',
  960. widget=Livesearch(
  961. query_key='q',
  962. query_url='dcim-api:device-list',
  963. field_to_update='device'
  964. )
  965. )
  966. port = ChainedModelChoiceField(
  967. queryset=ConsolePort.objects.all(),
  968. chains=(
  969. ('device', 'device'),
  970. ),
  971. label='Port',
  972. widget=APISelect(
  973. api_url='/api/dcim/console-ports/?device_id={{device}}',
  974. disabled_indicator='cs_port'
  975. )
  976. )
  977. connection_status = forms.BooleanField(
  978. required=False,
  979. initial=CONNECTION_STATUS_CONNECTED,
  980. label='Status',
  981. widget=forms.Select(
  982. choices=CONNECTION_STATUS_CHOICES
  983. )
  984. )
  985. class Meta:
  986. fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
  987. labels = {
  988. 'connection_status': 'Status',
  989. }
  990. class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
  991. pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
  992. #
  993. # Power ports
  994. #
  995. class PowerPortForm(BootstrapMixin, forms.ModelForm):
  996. class Meta:
  997. model = PowerPort
  998. fields = ['device', 'name']
  999. widgets = {
  1000. 'device': forms.HiddenInput(),
  1001. }
  1002. class PowerPortCreateForm(DeviceComponentForm):
  1003. name_pattern = ExpandableNameField(label='Name')
  1004. class PowerConnectionCSVForm(forms.ModelForm):
  1005. pdu = FlexibleModelChoiceField(
  1006. queryset=Device.objects.filter(device_type__is_pdu=True),
  1007. to_field_name='name',
  1008. help_text='PDU name or ID',
  1009. error_messages={
  1010. 'invalid_choice': 'PDU not found.',
  1011. }
  1012. )
  1013. power_outlet = forms.CharField(
  1014. help_text='Power outlet name'
  1015. )
  1016. device = FlexibleModelChoiceField(
  1017. queryset=Device.objects.all(),
  1018. to_field_name='name',
  1019. help_text='Device name or ID',
  1020. error_messages={
  1021. 'invalid_choice': 'Device not found',
  1022. }
  1023. )
  1024. power_port = forms.CharField(
  1025. help_text='Power port name'
  1026. )
  1027. connection_status = CSVChoiceField(
  1028. choices=CONNECTION_STATUS_CHOICES,
  1029. help_text='Connection status'
  1030. )
  1031. class Meta:
  1032. model = PowerPort
  1033. fields = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
  1034. def clean_power_port(self):
  1035. power_port_name = self.cleaned_data.get('power_port')
  1036. if not self.cleaned_data.get('device') or not power_port_name:
  1037. return None
  1038. try:
  1039. # Retrieve power port by name
  1040. powerport = PowerPort.objects.get(
  1041. device=self.cleaned_data['device'], name=power_port_name
  1042. )
  1043. # Check if the power port is already connected
  1044. if powerport.power_outlet is not None:
  1045. raise forms.ValidationError("{} {} is already connected".format(
  1046. self.cleaned_data['device'], power_port_name
  1047. ))
  1048. except PowerPort.DoesNotExist:
  1049. raise forms.ValidationError("Invalid power port ({} {})".format(
  1050. self.cleaned_data['device'], power_port_name
  1051. ))
  1052. self.instance = powerport
  1053. return powerport
  1054. def clean_power_outlet(self):
  1055. power_outlet_name = self.cleaned_data.get('power_outlet')
  1056. if not self.cleaned_data.get('pdu') or not power_outlet_name:
  1057. return None
  1058. try:
  1059. # Retrieve power outlet by name
  1060. power_outlet = PowerOutlet.objects.get(
  1061. device=self.cleaned_data['pdu'], name=power_outlet_name
  1062. )
  1063. # Check if the power outlet is already connected
  1064. if PowerPort.objects.filter(power_outlet=power_outlet).count():
  1065. raise forms.ValidationError("{} {} is already connected".format(
  1066. self.cleaned_data['pdu'], power_outlet_name
  1067. ))
  1068. except PowerOutlet.DoesNotExist:
  1069. raise forms.ValidationError("Invalid power outlet ({} {})".format(
  1070. self.cleaned_data['pdu'], power_outlet_name
  1071. ))
  1072. return power_outlet
  1073. class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  1074. site = forms.ModelChoiceField(
  1075. queryset=Site.objects.all(),
  1076. required=False,
  1077. widget=forms.Select(
  1078. attrs={'filter-for': 'rack'}
  1079. )
  1080. )
  1081. rack = ChainedModelChoiceField(
  1082. queryset=Rack.objects.all(),
  1083. chains=(
  1084. ('site', 'site'),
  1085. ),
  1086. label='Rack',
  1087. required=False,
  1088. widget=APISelect(
  1089. api_url='/api/dcim/racks/?site_id={{site}}',
  1090. attrs={'filter-for': 'pdu', 'nullable': 'true'}
  1091. )
  1092. )
  1093. pdu = ChainedModelChoiceField(
  1094. queryset=Device.objects.all(),
  1095. chains=(
  1096. ('site', 'site'),
  1097. ('rack', 'rack'),
  1098. ),
  1099. label='PDU',
  1100. required=False,
  1101. widget=APISelect(
  1102. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}&is_pdu=True',
  1103. display_field='display_name',
  1104. attrs={'filter-for': 'power_outlet'}
  1105. )
  1106. )
  1107. livesearch = forms.CharField(
  1108. required=False,
  1109. label='PDU',
  1110. widget=Livesearch(
  1111. query_key='q',
  1112. query_url='dcim-api:device-list',
  1113. field_to_update='pdu'
  1114. )
  1115. )
  1116. power_outlet = ChainedModelChoiceField(
  1117. queryset=PowerOutlet.objects.all(),
  1118. chains=(
  1119. ('device', 'pdu'),
  1120. ),
  1121. label='Outlet',
  1122. widget=APISelect(
  1123. api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
  1124. disabled_indicator='connected_port'
  1125. )
  1126. )
  1127. class Meta:
  1128. model = PowerPort
  1129. fields = ['site', 'rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
  1130. labels = {
  1131. 'power_outlet': 'Outlet',
  1132. 'connection_status': 'Status',
  1133. }
  1134. def __init__(self, *args, **kwargs):
  1135. super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
  1136. if not self.instance.pk:
  1137. raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
  1138. #
  1139. # Power outlets
  1140. #
  1141. class PowerOutletForm(BootstrapMixin, forms.ModelForm):
  1142. class Meta:
  1143. model = PowerOutlet
  1144. fields = ['device', 'name']
  1145. widgets = {
  1146. 'device': forms.HiddenInput(),
  1147. }
  1148. class PowerOutletCreateForm(DeviceComponentForm):
  1149. name_pattern = ExpandableNameField(label='Name')
  1150. class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  1151. site = forms.ModelChoiceField(
  1152. queryset=Site.objects.all(),
  1153. required=False,
  1154. widget=forms.Select(
  1155. attrs={'filter-for': 'rack'}
  1156. )
  1157. )
  1158. rack = ChainedModelChoiceField(
  1159. queryset=Rack.objects.all(),
  1160. chains=(
  1161. ('site', 'site'),
  1162. ),
  1163. label='Rack',
  1164. required=False,
  1165. widget=APISelect(
  1166. api_url='/api/dcim/racks/?site_id={{site}}',
  1167. attrs={'filter-for': 'device', 'nullable': 'true'}
  1168. )
  1169. )
  1170. device = ChainedModelChoiceField(
  1171. queryset=Device.objects.all(),
  1172. chains=(
  1173. ('site', 'site'),
  1174. ('rack', 'rack'),
  1175. ),
  1176. label='Device',
  1177. required=False,
  1178. widget=APISelect(
  1179. api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
  1180. display_field='display_name',
  1181. attrs={'filter-for': 'port'}
  1182. )
  1183. )
  1184. livesearch = forms.CharField(
  1185. required=False,
  1186. label='Device',
  1187. widget=Livesearch(
  1188. query_key='q',
  1189. query_url='dcim-api:device-list',
  1190. field_to_update='device'
  1191. )
  1192. )
  1193. port = ChainedModelChoiceField(
  1194. queryset=PowerPort.objects.all(),
  1195. chains=(
  1196. ('device', 'device'),
  1197. ),
  1198. label='Port',
  1199. widget=APISelect(
  1200. api_url='/api/dcim/power-ports/?device_id={{device}}',
  1201. disabled_indicator='power_outlet'
  1202. )
  1203. )
  1204. connection_status = forms.BooleanField(
  1205. required=False,
  1206. initial=CONNECTION_STATUS_CONNECTED,
  1207. label='Status',
  1208. widget=forms.Select(
  1209. choices=CONNECTION_STATUS_CHOICES
  1210. )
  1211. )
  1212. class Meta:
  1213. fields = ['site', 'rack', 'device', 'livesearch', 'port', 'connection_status']
  1214. labels = {
  1215. 'connection_status': 'Status',
  1216. }
  1217. class PowerOutletBulkDisconnectForm(ConfirmationForm):
  1218. pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
  1219. #
  1220. # Interfaces
  1221. #
  1222. class InterfaceForm(BootstrapMixin, forms.ModelForm):
  1223. class Meta:
  1224. model = Interface
  1225. fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description']
  1226. widgets = {
  1227. 'device': forms.HiddenInput(),
  1228. }
  1229. def __init__(self, *args, **kwargs):
  1230. super(InterfaceForm, self).__init__(*args, **kwargs)
  1231. # Limit LAG choices to interfaces belonging to this device
  1232. if self.is_bound:
  1233. self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
  1234. device_id=self.data['device'], form_factor=IFACE_FF_LAG
  1235. )
  1236. else:
  1237. self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
  1238. device=self.instance.device, form_factor=IFACE_FF_LAG
  1239. )
  1240. class InterfaceCreateForm(DeviceComponentForm):
  1241. name_pattern = ExpandableNameField(label='Name')
  1242. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  1243. enabled = forms.BooleanField(required=False)
  1244. lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
  1245. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  1246. mac_address = MACAddressFormField(required=False, label='MAC Address')
  1247. mgmt_only = forms.BooleanField(required=False, label='OOB Management')
  1248. description = forms.CharField(max_length=100, required=False)
  1249. def __init__(self, *args, **kwargs):
  1250. # Set interfaces enabled by default
  1251. kwargs['initial'] = kwargs.get('initial', {}).copy()
  1252. kwargs['initial'].update({'enabled': True})
  1253. super(InterfaceCreateForm, self).__init__(*args, **kwargs)
  1254. # Limit LAG choices to interfaces belonging to this device
  1255. if self.device is not None:
  1256. self.fields['lag'].queryset = Interface.objects.order_naturally().filter(
  1257. device=self.device, form_factor=IFACE_FF_LAG
  1258. )
  1259. else:
  1260. self.fields['lag'].queryset = Interface.objects.none()
  1261. class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
  1262. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  1263. device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
  1264. form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
  1265. enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
  1266. lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
  1267. mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
  1268. mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
  1269. description = forms.CharField(max_length=100, required=False)
  1270. class Meta:
  1271. nullable_fields = ['lag', 'mtu', 'description']
  1272. def __init__(self, *args, **kwargs):
  1273. super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
  1274. # Limit LAG choices to interfaces which belong to the parent device.
  1275. device = None
  1276. if self.initial.get('device'):
  1277. try:
  1278. device = Device.objects.get(pk=self.initial.get('device'))
  1279. except Device.DoesNotExist:
  1280. pass
  1281. if device is not None:
  1282. interface_ordering = device.device_type.interface_ordering
  1283. self.fields['lag'].queryset = Interface.objects.order_naturally(method=interface_ordering).filter(
  1284. device=device, form_factor=IFACE_FF_LAG
  1285. )
  1286. else:
  1287. self.fields['lag'].choices = []
  1288. class InterfaceBulkDisconnectForm(ConfirmationForm):
  1289. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  1290. #
  1291. # Interface connections
  1292. #
  1293. class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  1294. interface_a = forms.ChoiceField(
  1295. choices=[],
  1296. widget=SelectWithDisabled,
  1297. label='Interface'
  1298. )
  1299. site_b = forms.ModelChoiceField(
  1300. queryset=Site.objects.all(),
  1301. label='Site',
  1302. required=False,
  1303. widget=forms.Select(
  1304. attrs={'filter-for': 'rack_b'}
  1305. )
  1306. )
  1307. rack_b = ChainedModelChoiceField(
  1308. queryset=Rack.objects.all(),
  1309. chains=(
  1310. ('site', 'site_b'),
  1311. ),
  1312. label='Rack',
  1313. required=False,
  1314. widget=APISelect(
  1315. api_url='/api/dcim/racks/?site_id={{site_b}}',
  1316. attrs={'filter-for': 'device_b', 'nullable': 'true'}
  1317. )
  1318. )
  1319. device_b = ChainedModelChoiceField(
  1320. queryset=Device.objects.all(),
  1321. chains=(
  1322. ('site', 'site_b'),
  1323. ('rack', 'rack_b'),
  1324. ),
  1325. label='Device',
  1326. required=False,
  1327. widget=APISelect(
  1328. api_url='/api/dcim/devices/?site_id={{site_b}}&rack_id={{rack_b}}',
  1329. display_field='display_name',
  1330. attrs={'filter-for': 'interface_b'}
  1331. )
  1332. )
  1333. livesearch = forms.CharField(
  1334. required=False,
  1335. label='Device',
  1336. widget=Livesearch(
  1337. query_key='q',
  1338. query_url='dcim-api:device-list',
  1339. field_to_update='device_b'
  1340. )
  1341. )
  1342. interface_b = ChainedModelChoiceField(
  1343. queryset=Interface.objects.connectable().select_related(
  1344. 'circuit_termination', 'connected_as_a', 'connected_as_b'
  1345. ),
  1346. chains=(
  1347. ('device', 'device_b'),
  1348. ),
  1349. label='Interface',
  1350. widget=APISelect(
  1351. api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
  1352. disabled_indicator='is_connected'
  1353. )
  1354. )
  1355. class Meta:
  1356. model = InterfaceConnection
  1357. fields = ['interface_a', 'site_b', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
  1358. def __init__(self, device_a, *args, **kwargs):
  1359. super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
  1360. # Initialize interface A choices
  1361. device_a_interfaces = Interface.objects.connectable().order_naturally().filter(device=device_a).select_related(
  1362. 'circuit_termination', 'connected_as_a', 'connected_as_b'
  1363. )
  1364. self.fields['interface_a'].choices = [
  1365. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
  1366. ]
  1367. # Mark connected interfaces as disabled
  1368. if self.data.get('device_b'):
  1369. self.fields['interface_b'].choices = [
  1370. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
  1371. ]
  1372. class InterfaceConnectionCSVForm(forms.ModelForm):
  1373. device_a = FlexibleModelChoiceField(
  1374. queryset=Device.objects.all(),
  1375. to_field_name='name',
  1376. help_text='Name or ID of device A',
  1377. error_messages={'invalid_choice': 'Device A not found.'}
  1378. )
  1379. interface_a = forms.CharField(
  1380. help_text='Name of interface A'
  1381. )
  1382. device_b = FlexibleModelChoiceField(
  1383. queryset=Device.objects.all(),
  1384. to_field_name='name',
  1385. help_text='Name or ID of device B',
  1386. error_messages={'invalid_choice': 'Device B not found.'}
  1387. )
  1388. interface_b = forms.CharField(
  1389. help_text='Name of interface B'
  1390. )
  1391. connection_status = CSVChoiceField(
  1392. choices=CONNECTION_STATUS_CHOICES,
  1393. help_text='Connection status'
  1394. )
  1395. class Meta:
  1396. model = InterfaceConnection
  1397. fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
  1398. def clean_interface_a(self):
  1399. interface_name = self.cleaned_data.get('interface_a')
  1400. if not interface_name:
  1401. return None
  1402. try:
  1403. # Retrieve interface by name
  1404. interface = Interface.objects.get(
  1405. device=self.cleaned_data['device_a'], name=interface_name
  1406. )
  1407. # Check for an existing connection to this interface
  1408. if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
  1409. raise forms.ValidationError("{} {} is already connected".format(
  1410. self.cleaned_data['device_a'], interface_name
  1411. ))
  1412. except Interface.DoesNotExist:
  1413. raise forms.ValidationError("Invalid interface ({} {})".format(
  1414. self.cleaned_data['device_a'], interface_name
  1415. ))
  1416. return interface
  1417. def clean_interface_b(self):
  1418. interface_name = self.cleaned_data.get('interface_b')
  1419. if not interface_name:
  1420. return None
  1421. try:
  1422. # Retrieve interface by name
  1423. interface = Interface.objects.get(
  1424. device=self.cleaned_data['device_b'], name=interface_name
  1425. )
  1426. # Check for an existing connection to this interface
  1427. if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
  1428. raise forms.ValidationError("{} {} is already connected".format(
  1429. self.cleaned_data['device_b'], interface_name
  1430. ))
  1431. except Interface.DoesNotExist:
  1432. raise forms.ValidationError("Invalid interface ({} {})".format(
  1433. self.cleaned_data['device_b'], interface_name
  1434. ))
  1435. return interface
  1436. class InterfaceConnectionDeletionForm(ConfirmationForm):
  1437. # Used for HTTP redirect upon successful deletion
  1438. device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
  1439. #
  1440. # Device bays
  1441. #
  1442. class DeviceBayForm(BootstrapMixin, forms.ModelForm):
  1443. class Meta:
  1444. model = DeviceBay
  1445. fields = ['device', 'name']
  1446. widgets = {
  1447. 'device': forms.HiddenInput(),
  1448. }
  1449. class DeviceBayCreateForm(DeviceComponentForm):
  1450. name_pattern = ExpandableNameField(label='Name')
  1451. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  1452. installed_device = forms.ModelChoiceField(
  1453. queryset=Device.objects.all(),
  1454. label='Child Device',
  1455. help_text="Child devices must first be created and assigned to the site/rack of the parent device."
  1456. )
  1457. def __init__(self, device_bay, *args, **kwargs):
  1458. super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
  1459. self.fields['installed_device'].queryset = Device.objects.filter(
  1460. site=device_bay.device.site,
  1461. rack=device_bay.device.rack,
  1462. parent_bay__isnull=True,
  1463. device_type__u_height=0,
  1464. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
  1465. ).exclude(pk=device_bay.device.pk)
  1466. #
  1467. # Connections
  1468. #
  1469. class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
  1470. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1471. device = forms.CharField(required=False, label='Device name')
  1472. class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
  1473. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1474. device = forms.CharField(required=False, label='Device name')
  1475. class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
  1476. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1477. device = forms.CharField(required=False, label='Device name')
  1478. #
  1479. # Inventory items
  1480. #
  1481. class InventoryItemForm(BootstrapMixin, forms.ModelForm):
  1482. class Meta:
  1483. model = InventoryItem
  1484. fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']