forms.py 61 KB

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