forms.py 76 KB

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