forms.py 74 KB

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