forms.py 113 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068
  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 Q
  8. from mptt.forms import TreeNodeChoiceField
  9. from netaddr import EUI
  10. from netaddr.core import AddrFormatError
  11. from taggit.forms import TagField
  12. from timezone_field import TimeZoneFormField
  13. from circuits.models import Circuit, Provider
  14. from extras.forms import (
  15. AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm
  16. )
  17. from ipam.models import IPAddress, VLAN, VLANGroup
  18. from tenancy.forms import TenancyFilterForm, TenancyForm
  19. from tenancy.models import Tenant, TenantGroup
  20. from utilities.forms import (
  21. APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
  22. BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
  23. ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
  24. SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
  25. )
  26. from virtualization.models import Cluster, ClusterGroup
  27. from .choices import *
  28. from .constants import *
  29. from .models import (
  30. Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
  31. Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
  32. InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate,
  33. Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
  34. )
  35. DEVICE_BY_PK_RE = r'{\d+\}'
  36. INTERFACE_MODE_HELP_TEXT = """
  37. Access: One untagged VLAN<br />
  38. Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
  39. Tagged All: Implies all VLANs are available (w/optional untagged VLAN)
  40. """
  41. def get_device_by_name_or_pk(name):
  42. """
  43. Attempt to retrieve a device by either its name or primary key ('{pk}').
  44. """
  45. if re.match(DEVICE_BY_PK_RE, name):
  46. pk = name.strip('{}')
  47. device = Device.objects.get(pk=pk)
  48. else:
  49. device = Device.objects.get(name=name)
  50. return device
  51. class InterfaceCommonForm:
  52. def clean(self):
  53. super().clean()
  54. # Validate VLAN assignments
  55. tagged_vlans = self.cleaned_data['tagged_vlans']
  56. # Untagged interfaces cannot be assigned tagged VLANs
  57. if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans:
  58. raise forms.ValidationError({
  59. 'mode': "An access interface cannot have tagged VLANs assigned."
  60. })
  61. # Remove all tagged VLAN assignments from "tagged all" interfaces
  62. elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL:
  63. self.cleaned_data['tagged_vlans'] = []
  64. class BulkRenameForm(forms.Form):
  65. """
  66. An extendable form to be used for renaming device components in bulk.
  67. """
  68. find = forms.CharField()
  69. replace = forms.CharField()
  70. use_regex = forms.BooleanField(
  71. required=False,
  72. initial=True,
  73. label='Use regular expressions'
  74. )
  75. def clean(self):
  76. # Validate regular expression in "find" field
  77. if self.cleaned_data['use_regex']:
  78. try:
  79. re.compile(self.cleaned_data['find'])
  80. except re.error:
  81. raise forms.ValidationError({
  82. 'find': "Invalid regular expression"
  83. })
  84. #
  85. # Fields
  86. #
  87. class MACAddressField(forms.Field):
  88. widget = forms.CharField
  89. default_error_messages = {
  90. 'invalid': 'MAC address must be in EUI-48 format',
  91. }
  92. def to_python(self, value):
  93. value = super().to_python(value)
  94. # Validate MAC address format
  95. try:
  96. value = EUI(value.strip())
  97. except AddrFormatError:
  98. raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
  99. return value
  100. #
  101. # Regions
  102. #
  103. class RegionForm(BootstrapMixin, forms.ModelForm):
  104. slug = SlugField()
  105. class Meta:
  106. model = Region
  107. fields = [
  108. 'parent', 'name', 'slug',
  109. ]
  110. widgets = {
  111. 'parent': APISelect(
  112. api_url="/api/dcim/regions/"
  113. )
  114. }
  115. class RegionCSVForm(forms.ModelForm):
  116. parent = forms.ModelChoiceField(
  117. queryset=Region.objects.all(),
  118. required=False,
  119. to_field_name='name',
  120. help_text='Name of parent region',
  121. error_messages={
  122. 'invalid_choice': 'Region not found.',
  123. }
  124. )
  125. class Meta:
  126. model = Region
  127. fields = Region.csv_headers
  128. help_texts = {
  129. 'name': 'Region name',
  130. 'slug': 'URL-friendly slug',
  131. }
  132. class RegionFilterForm(BootstrapMixin, forms.Form):
  133. model = Site
  134. q = forms.CharField(
  135. required=False,
  136. label='Search'
  137. )
  138. #
  139. # Sites
  140. #
  141. class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  142. region = TreeNodeChoiceField(
  143. queryset=Region.objects.all(),
  144. required=False,
  145. widget=APISelect(
  146. api_url="/api/dcim/regions/"
  147. )
  148. )
  149. slug = SlugField()
  150. comments = CommentField()
  151. tags = TagField(
  152. required=False
  153. )
  154. class Meta:
  155. model = Site
  156. fields = [
  157. 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
  158. 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
  159. 'contact_email', 'comments', 'tags',
  160. ]
  161. widgets = {
  162. 'physical_address': SmallTextarea(
  163. attrs={
  164. 'rows': 3,
  165. }
  166. ),
  167. 'shipping_address': SmallTextarea(
  168. attrs={
  169. 'rows': 3,
  170. }
  171. ),
  172. 'status': StaticSelect2(),
  173. 'time_zone': StaticSelect2(),
  174. }
  175. help_texts = {
  176. 'name': "Full name of the site",
  177. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  178. 'asn': "BGP autonomous system number",
  179. 'time_zone': "Local time zone",
  180. 'description': "Short description (will appear in sites list)",
  181. 'physical_address': "Physical location of the building (e.g. for GPS)",
  182. 'shipping_address': "If different from the physical address",
  183. 'latitude': "Latitude in decimal format (xx.yyyyyy)",
  184. 'longitude': "Longitude in decimal format (xx.yyyyyy)"
  185. }
  186. class SiteCSVForm(forms.ModelForm):
  187. status = CSVChoiceField(
  188. choices=SITE_STATUS_CHOICES,
  189. required=False,
  190. help_text='Operational status'
  191. )
  192. region = forms.ModelChoiceField(
  193. queryset=Region.objects.all(),
  194. required=False,
  195. to_field_name='name',
  196. help_text='Name of assigned region',
  197. error_messages={
  198. 'invalid_choice': 'Region not found.',
  199. }
  200. )
  201. tenant = forms.ModelChoiceField(
  202. queryset=Tenant.objects.all(),
  203. required=False,
  204. to_field_name='name',
  205. help_text='Name of assigned tenant',
  206. error_messages={
  207. 'invalid_choice': 'Tenant not found.',
  208. }
  209. )
  210. class Meta:
  211. model = Site
  212. fields = Site.csv_headers
  213. help_texts = {
  214. 'name': 'Site name',
  215. 'slug': 'URL-friendly slug',
  216. 'asn': '32-bit autonomous system number',
  217. }
  218. class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  219. pk = forms.ModelMultipleChoiceField(
  220. queryset=Site.objects.all(),
  221. widget=forms.MultipleHiddenInput
  222. )
  223. status = forms.ChoiceField(
  224. choices=add_blank_choice(SITE_STATUS_CHOICES),
  225. required=False,
  226. initial='',
  227. widget=StaticSelect2()
  228. )
  229. region = TreeNodeChoiceField(
  230. queryset=Region.objects.all(),
  231. required=False,
  232. widget=APISelect(
  233. api_url="/api/dcim/regions/"
  234. )
  235. )
  236. tenant = forms.ModelChoiceField(
  237. queryset=Tenant.objects.all(),
  238. required=False,
  239. widget=APISelect(
  240. api_url="/api/tenancy/tenants",
  241. )
  242. )
  243. asn = forms.IntegerField(
  244. min_value=1,
  245. max_value=4294967295,
  246. required=False,
  247. label='ASN'
  248. )
  249. description = forms.CharField(
  250. max_length=100,
  251. required=False
  252. )
  253. time_zone = TimeZoneFormField(
  254. choices=add_blank_choice(TimeZoneFormField().choices),
  255. required=False,
  256. widget=StaticSelect2()
  257. )
  258. class Meta:
  259. nullable_fields = [
  260. 'region', 'tenant', 'asn', 'description', 'time_zone',
  261. ]
  262. class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  263. model = Site
  264. field_order = ['q', 'status', 'region', 'tenant_group', 'tenant']
  265. q = forms.CharField(
  266. required=False,
  267. label='Search'
  268. )
  269. status = forms.MultipleChoiceField(
  270. choices=SITE_STATUS_CHOICES,
  271. required=False,
  272. widget=StaticSelect2Multiple()
  273. )
  274. region = forms.ModelMultipleChoiceField(
  275. queryset=Region.objects.all(),
  276. to_field_name='slug',
  277. required=False,
  278. widget=APISelectMultiple(
  279. api_url="/api/dcim/regions/",
  280. value_field="slug",
  281. )
  282. )
  283. #
  284. # Rack groups
  285. #
  286. class RackGroupForm(BootstrapMixin, forms.ModelForm):
  287. slug = SlugField()
  288. class Meta:
  289. model = RackGroup
  290. fields = [
  291. 'site', 'name', 'slug',
  292. ]
  293. widgets = {
  294. 'site': APISelect(
  295. api_url="/api/dcim/sites/"
  296. )
  297. }
  298. class RackGroupCSVForm(forms.ModelForm):
  299. site = forms.ModelChoiceField(
  300. queryset=Site.objects.all(),
  301. to_field_name='name',
  302. help_text='Name of parent site',
  303. error_messages={
  304. 'invalid_choice': 'Site not found.',
  305. }
  306. )
  307. class Meta:
  308. model = RackGroup
  309. fields = RackGroup.csv_headers
  310. help_texts = {
  311. 'name': 'Name of rack group',
  312. 'slug': 'URL-friendly slug',
  313. }
  314. class RackGroupFilterForm(BootstrapMixin, forms.Form):
  315. site = FilterChoiceField(
  316. queryset=Site.objects.all(),
  317. to_field_name='slug',
  318. widget=APISelectMultiple(
  319. api_url="/api/dcim/sites/",
  320. value_field="slug",
  321. )
  322. )
  323. #
  324. # Rack roles
  325. #
  326. class RackRoleForm(BootstrapMixin, forms.ModelForm):
  327. slug = SlugField()
  328. class Meta:
  329. model = RackRole
  330. fields = [
  331. 'name', 'slug', 'color',
  332. ]
  333. class RackRoleCSVForm(forms.ModelForm):
  334. slug = SlugField()
  335. class Meta:
  336. model = RackRole
  337. fields = RackRole.csv_headers
  338. help_texts = {
  339. 'name': 'Name of rack role',
  340. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  341. }
  342. #
  343. # Racks
  344. #
  345. class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  346. group = ChainedModelChoiceField(
  347. queryset=RackGroup.objects.all(),
  348. chains=(
  349. ('site', 'site'),
  350. ),
  351. required=False,
  352. widget=APISelect(
  353. api_url='/api/dcim/rack-groups/',
  354. )
  355. )
  356. comments = CommentField()
  357. tags = TagField(
  358. required=False
  359. )
  360. class Meta:
  361. model = Rack
  362. fields = [
  363. 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag',
  364. 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags',
  365. ]
  366. help_texts = {
  367. 'site': "The site at which the rack exists",
  368. 'name': "Organizational rack name",
  369. 'facility_id': "The unique rack ID assigned by the facility",
  370. 'u_height': "Height in rack units",
  371. }
  372. widgets = {
  373. 'site': APISelect(
  374. api_url="/api/dcim/sites/",
  375. filter_for={
  376. 'group': 'site_id',
  377. }
  378. ),
  379. 'status': StaticSelect2(),
  380. 'role': APISelect(
  381. api_url="/api/dcim/rack-roles/"
  382. ),
  383. 'type': StaticSelect2(),
  384. 'width': StaticSelect2(),
  385. 'outer_unit': StaticSelect2(),
  386. }
  387. class RackCSVForm(forms.ModelForm):
  388. site = forms.ModelChoiceField(
  389. queryset=Site.objects.all(),
  390. to_field_name='name',
  391. help_text='Name of parent site',
  392. error_messages={
  393. 'invalid_choice': 'Site not found.',
  394. }
  395. )
  396. group_name = forms.CharField(
  397. help_text='Name of rack group',
  398. required=False
  399. )
  400. tenant = forms.ModelChoiceField(
  401. queryset=Tenant.objects.all(),
  402. required=False,
  403. to_field_name='name',
  404. help_text='Name of assigned tenant',
  405. error_messages={
  406. 'invalid_choice': 'Tenant not found.',
  407. }
  408. )
  409. status = CSVChoiceField(
  410. choices=RACK_STATUS_CHOICES,
  411. required=False,
  412. help_text='Operational status'
  413. )
  414. role = forms.ModelChoiceField(
  415. queryset=RackRole.objects.all(),
  416. required=False,
  417. to_field_name='name',
  418. help_text='Name of assigned role',
  419. error_messages={
  420. 'invalid_choice': 'Role not found.',
  421. }
  422. )
  423. type = CSVChoiceField(
  424. choices=RACK_TYPE_CHOICES,
  425. required=False,
  426. help_text='Rack type'
  427. )
  428. width = forms.ChoiceField(
  429. choices=(
  430. (RACK_WIDTH_19IN, '19'),
  431. (RACK_WIDTH_23IN, '23'),
  432. ),
  433. help_text='Rail-to-rail width (in inches)'
  434. )
  435. outer_unit = CSVChoiceField(
  436. choices=RACK_DIMENSION_UNIT_CHOICES,
  437. required=False,
  438. help_text='Unit for outer dimensions'
  439. )
  440. class Meta:
  441. model = Rack
  442. fields = Rack.csv_headers
  443. help_texts = {
  444. 'name': 'Rack name',
  445. 'u_height': 'Height in rack units',
  446. }
  447. def clean(self):
  448. super().clean()
  449. site = self.cleaned_data.get('site')
  450. group_name = self.cleaned_data.get('group_name')
  451. name = self.cleaned_data.get('name')
  452. facility_id = self.cleaned_data.get('facility_id')
  453. # Validate rack group
  454. if group_name:
  455. try:
  456. self.instance.group = RackGroup.objects.get(site=site, name=group_name)
  457. except RackGroup.DoesNotExist:
  458. raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
  459. # Validate uniqueness of rack name within group
  460. if Rack.objects.filter(group=self.instance.group, name=name).exists():
  461. raise forms.ValidationError(
  462. "A rack named {} already exists within group {}".format(name, group_name)
  463. )
  464. # Validate uniqueness of facility ID within group
  465. if facility_id and Rack.objects.filter(group=self.instance.group, facility_id=facility_id).exists():
  466. raise forms.ValidationError(
  467. "A rack with the facility ID {} already exists within group {}".format(facility_id, group_name)
  468. )
  469. class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  470. pk = forms.ModelMultipleChoiceField(
  471. queryset=Rack.objects.all(),
  472. widget=forms.MultipleHiddenInput
  473. )
  474. site = forms.ModelChoiceField(
  475. queryset=Site.objects.all(),
  476. required=False,
  477. widget=APISelect(
  478. api_url="/api/dcim/sites",
  479. filter_for={
  480. 'group': 'site_id',
  481. }
  482. )
  483. )
  484. group = forms.ModelChoiceField(
  485. queryset=RackGroup.objects.all(),
  486. required=False,
  487. widget=APISelect(
  488. api_url="/api/dcim/rack-groups",
  489. )
  490. )
  491. tenant = forms.ModelChoiceField(
  492. queryset=Tenant.objects.all(),
  493. required=False,
  494. widget=APISelect(
  495. api_url="/api/tenancy/tenants",
  496. )
  497. )
  498. status = forms.ChoiceField(
  499. choices=add_blank_choice(RACK_STATUS_CHOICES),
  500. required=False,
  501. initial='',
  502. widget=StaticSelect2()
  503. )
  504. role = forms.ModelChoiceField(
  505. queryset=RackRole.objects.all(),
  506. required=False,
  507. widget=APISelect(
  508. api_url="/api/dcim/rack-roles",
  509. )
  510. )
  511. serial = forms.CharField(
  512. max_length=50,
  513. required=False,
  514. label='Serial Number'
  515. )
  516. asset_tag = forms.CharField(
  517. max_length=50,
  518. required=False
  519. )
  520. type = forms.ChoiceField(
  521. choices=add_blank_choice(RACK_TYPE_CHOICES),
  522. required=False,
  523. widget=StaticSelect2()
  524. )
  525. width = forms.ChoiceField(
  526. choices=add_blank_choice(RACK_WIDTH_CHOICES),
  527. required=False,
  528. widget=StaticSelect2()
  529. )
  530. u_height = forms.IntegerField(
  531. required=False,
  532. label='Height (U)'
  533. )
  534. desc_units = forms.NullBooleanField(
  535. required=False,
  536. widget=BulkEditNullBooleanSelect,
  537. label='Descending units'
  538. )
  539. outer_width = forms.IntegerField(
  540. required=False,
  541. min_value=1
  542. )
  543. outer_depth = forms.IntegerField(
  544. required=False,
  545. min_value=1
  546. )
  547. outer_unit = forms.ChoiceField(
  548. choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES),
  549. required=False,
  550. widget=StaticSelect2()
  551. )
  552. comments = CommentField(
  553. widget=SmallTextarea
  554. )
  555. class Meta:
  556. nullable_fields = [
  557. 'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
  558. ]
  559. class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  560. model = Rack
  561. field_order = ['q', 'site', 'group_id', 'status', 'role', 'tenant_group', 'tenant']
  562. q = forms.CharField(
  563. required=False,
  564. label='Search'
  565. )
  566. site = FilterChoiceField(
  567. queryset=Site.objects.all(),
  568. to_field_name='slug',
  569. widget=APISelectMultiple(
  570. api_url="/api/dcim/sites/",
  571. value_field="slug",
  572. filter_for={
  573. 'group_id': 'site'
  574. }
  575. )
  576. )
  577. group_id = ChainedModelChoiceField(
  578. label='Rack group',
  579. queryset=RackGroup.objects.prefetch_related('site'),
  580. chains=(
  581. ('site', 'site'),
  582. ),
  583. required=False,
  584. widget=APISelectMultiple(
  585. api_url="/api/dcim/rack-groups/",
  586. null_option=True,
  587. )
  588. )
  589. status = forms.MultipleChoiceField(
  590. choices=RACK_STATUS_CHOICES,
  591. required=False,
  592. widget=StaticSelect2Multiple()
  593. )
  594. role = FilterChoiceField(
  595. queryset=RackRole.objects.all(),
  596. to_field_name='slug',
  597. null_label='-- None --',
  598. widget=APISelectMultiple(
  599. api_url="/api/dcim/rack-roles/",
  600. value_field="slug",
  601. null_option=True,
  602. )
  603. )
  604. #
  605. # Rack reservations
  606. #
  607. class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm):
  608. units = SimpleArrayField(
  609. base_field=forms.IntegerField(),
  610. widget=ArrayFieldSelectMultiple(
  611. attrs={
  612. 'size': 10,
  613. }
  614. )
  615. )
  616. user = forms.ModelChoiceField(
  617. queryset=User.objects.order_by(
  618. 'username'
  619. ),
  620. widget=StaticSelect2()
  621. )
  622. class Meta:
  623. model = RackReservation
  624. fields = [
  625. 'units', 'user', 'tenant_group', 'tenant', 'description',
  626. ]
  627. def __init__(self, *args, **kwargs):
  628. super().__init__(*args, **kwargs)
  629. # Populate rack unit choices
  630. self.fields['units'].widget.choices = self._get_unit_choices()
  631. def _get_unit_choices(self):
  632. rack = self.instance.rack
  633. reserved_units = []
  634. for resv in rack.reservations.exclude(pk=self.instance.pk):
  635. for u in resv.units:
  636. reserved_units.append(u)
  637. unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
  638. return unit_choices
  639. class RackReservationBulkEditForm(BootstrapMixin, BulkEditForm):
  640. pk = forms.ModelMultipleChoiceField(
  641. queryset=RackReservation.objects.all(),
  642. widget=forms.MultipleHiddenInput()
  643. )
  644. user = forms.ModelChoiceField(
  645. queryset=User.objects.order_by(
  646. 'username'
  647. ),
  648. required=False,
  649. widget=StaticSelect2()
  650. )
  651. tenant = forms.ModelChoiceField(
  652. queryset=Tenant.objects.all(),
  653. required=False,
  654. widget=APISelect(
  655. api_url="/api/tenancy/tenant",
  656. )
  657. )
  658. description = forms.CharField(
  659. max_length=100,
  660. required=False
  661. )
  662. class Meta:
  663. nullable_fields = []
  664. class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm):
  665. field_order = ['q', 'site', 'group_id', 'tenant_group', 'tenant']
  666. q = forms.CharField(
  667. required=False,
  668. label='Search'
  669. )
  670. site = FilterChoiceField(
  671. queryset=Site.objects.all(),
  672. to_field_name='slug',
  673. widget=APISelectMultiple(
  674. api_url="/api/dcim/sites/",
  675. value_field="slug",
  676. )
  677. )
  678. group_id = FilterChoiceField(
  679. queryset=RackGroup.objects.prefetch_related('site'),
  680. label='Rack group',
  681. null_label='-- None --',
  682. widget=APISelectMultiple(
  683. api_url="/api/dcim/rack-groups/",
  684. null_option=True,
  685. )
  686. )
  687. #
  688. # Manufacturers
  689. #
  690. class ManufacturerForm(BootstrapMixin, forms.ModelForm):
  691. slug = SlugField()
  692. class Meta:
  693. model = Manufacturer
  694. fields = [
  695. 'name', 'slug',
  696. ]
  697. class ManufacturerCSVForm(forms.ModelForm):
  698. class Meta:
  699. model = Manufacturer
  700. fields = Manufacturer.csv_headers
  701. help_texts = {
  702. 'name': 'Manufacturer name',
  703. 'slug': 'URL-friendly slug',
  704. }
  705. #
  706. # Device types
  707. #
  708. class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
  709. slug = SlugField(
  710. slug_source='model'
  711. )
  712. comments = CommentField()
  713. tags = TagField(
  714. required=False
  715. )
  716. class Meta:
  717. model = DeviceType
  718. fields = [
  719. 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
  720. 'tags',
  721. ]
  722. widgets = {
  723. 'manufacturer': APISelect(
  724. api_url="/api/dcim/manufacturers/"
  725. ),
  726. 'subdevice_role': StaticSelect2()
  727. }
  728. class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
  729. manufacturer = forms.ModelChoiceField(
  730. queryset=Manufacturer.objects.all(),
  731. to_field_name='name'
  732. )
  733. class Meta:
  734. model = DeviceType
  735. fields = [
  736. 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
  737. ]
  738. class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  739. pk = forms.ModelMultipleChoiceField(
  740. queryset=DeviceType.objects.all(),
  741. widget=forms.MultipleHiddenInput()
  742. )
  743. manufacturer = forms.ModelChoiceField(
  744. queryset=Manufacturer.objects.all(),
  745. required=False,
  746. widget=APISelect(
  747. api_url="/api/dcim/manufactureres"
  748. )
  749. )
  750. u_height = forms.IntegerField(
  751. min_value=1,
  752. required=False
  753. )
  754. is_full_depth = forms.NullBooleanField(
  755. required=False,
  756. widget=BulkEditNullBooleanSelect(),
  757. label='Is full depth'
  758. )
  759. class Meta:
  760. nullable_fields = []
  761. class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
  762. model = DeviceType
  763. q = forms.CharField(
  764. required=False,
  765. label='Search'
  766. )
  767. manufacturer = FilterChoiceField(
  768. queryset=Manufacturer.objects.all(),
  769. to_field_name='slug',
  770. widget=APISelectMultiple(
  771. api_url="/api/dcim/manufacturers/",
  772. value_field="slug",
  773. )
  774. )
  775. subdevice_role = forms.NullBooleanField(
  776. required=False,
  777. label='Subdevice role',
  778. widget=StaticSelect2(
  779. choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES)
  780. )
  781. )
  782. console_ports = forms.NullBooleanField(
  783. required=False,
  784. label='Has console ports',
  785. widget=StaticSelect2(
  786. choices=BOOLEAN_WITH_BLANK_CHOICES
  787. )
  788. )
  789. console_server_ports = forms.NullBooleanField(
  790. required=False,
  791. label='Has console server ports',
  792. widget=StaticSelect2(
  793. choices=BOOLEAN_WITH_BLANK_CHOICES
  794. )
  795. )
  796. power_ports = forms.NullBooleanField(
  797. required=False,
  798. label='Has power ports',
  799. widget=StaticSelect2(
  800. choices=BOOLEAN_WITH_BLANK_CHOICES
  801. )
  802. )
  803. power_outlets = forms.NullBooleanField(
  804. required=False,
  805. label='Has power outlets',
  806. widget=StaticSelect2(
  807. choices=BOOLEAN_WITH_BLANK_CHOICES
  808. )
  809. )
  810. interfaces = forms.NullBooleanField(
  811. required=False,
  812. label='Has interfaces',
  813. widget=StaticSelect2(
  814. choices=BOOLEAN_WITH_BLANK_CHOICES
  815. )
  816. )
  817. pass_through_ports = forms.NullBooleanField(
  818. required=False,
  819. label='Has pass-through ports',
  820. widget=StaticSelect2(
  821. choices=BOOLEAN_WITH_BLANK_CHOICES
  822. )
  823. )
  824. #
  825. # Device component templates
  826. #
  827. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  828. class Meta:
  829. model = ConsolePortTemplate
  830. fields = [
  831. 'device_type', 'name', 'type',
  832. ]
  833. widgets = {
  834. 'device_type': forms.HiddenInput(),
  835. }
  836. class ConsolePortTemplateCreateForm(ComponentForm):
  837. name_pattern = ExpandableNameField(
  838. label='Name'
  839. )
  840. type = forms.ChoiceField(
  841. choices=CONSOLE_TYPE_CHOICES,
  842. widget=StaticSelect2()
  843. )
  844. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  845. class Meta:
  846. model = ConsoleServerPortTemplate
  847. fields = [
  848. 'device_type', 'name', 'type',
  849. ]
  850. widgets = {
  851. 'device_type': forms.HiddenInput(),
  852. }
  853. class ConsoleServerPortTemplateCreateForm(ComponentForm):
  854. name_pattern = ExpandableNameField(
  855. label='Name'
  856. )
  857. type = forms.ChoiceField(
  858. choices=CONSOLE_TYPE_CHOICES,
  859. widget=StaticSelect2()
  860. )
  861. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  862. class Meta:
  863. model = PowerPortTemplate
  864. fields = [
  865. 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
  866. ]
  867. widgets = {
  868. 'device_type': forms.HiddenInput(),
  869. }
  870. class PowerPortTemplateCreateForm(ComponentForm):
  871. name_pattern = ExpandableNameField(
  872. label='Name'
  873. )
  874. type = forms.ChoiceField(
  875. choices=add_blank_choice(PowerPortTypes.CHOICES),
  876. required=False
  877. )
  878. maximum_draw = forms.IntegerField(
  879. min_value=1,
  880. required=False,
  881. help_text="Maximum current draw (watts)"
  882. )
  883. allocated_draw = forms.IntegerField(
  884. min_value=1,
  885. required=False,
  886. help_text="Allocated current draw (watts)"
  887. )
  888. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  889. class Meta:
  890. model = PowerOutletTemplate
  891. fields = [
  892. 'device_type', 'name', 'type', 'power_port', 'feed_leg',
  893. ]
  894. widgets = {
  895. 'device_type': forms.HiddenInput(),
  896. }
  897. def __init__(self, *args, **kwargs):
  898. super().__init__(*args, **kwargs)
  899. # Limit power_port choices to current DeviceType
  900. if hasattr(self.instance, 'device_type'):
  901. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  902. device_type=self.instance.device_type
  903. )
  904. class PowerOutletTemplateCreateForm(ComponentForm):
  905. name_pattern = ExpandableNameField(
  906. label='Name'
  907. )
  908. type = forms.ChoiceField(
  909. choices=add_blank_choice(PowerOutletTypes.CHOICES),
  910. required=False
  911. )
  912. power_port = forms.ModelChoiceField(
  913. queryset=PowerPortTemplate.objects.all(),
  914. required=False
  915. )
  916. feed_leg = forms.ChoiceField(
  917. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  918. required=False,
  919. widget=StaticSelect2()
  920. )
  921. def __init__(self, *args, **kwargs):
  922. super().__init__(*args, **kwargs)
  923. # Limit power_port choices to current DeviceType
  924. self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
  925. device_type=self.parent
  926. )
  927. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  928. class Meta:
  929. model = InterfaceTemplate
  930. fields = [
  931. 'device_type', 'name', 'type', 'mgmt_only',
  932. ]
  933. widgets = {
  934. 'device_type': forms.HiddenInput(),
  935. 'type': StaticSelect2(),
  936. }
  937. class InterfaceTemplateCreateForm(ComponentForm):
  938. name_pattern = ExpandableNameField(
  939. label='Name'
  940. )
  941. type = forms.ChoiceField(
  942. choices=IFACE_TYPE_CHOICES,
  943. widget=StaticSelect2()
  944. )
  945. mgmt_only = forms.BooleanField(
  946. required=False,
  947. label='Management only'
  948. )
  949. class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  950. pk = forms.ModelMultipleChoiceField(
  951. queryset=InterfaceTemplate.objects.all(),
  952. widget=forms.MultipleHiddenInput()
  953. )
  954. type = forms.ChoiceField(
  955. choices=add_blank_choice(IFACE_TYPE_CHOICES),
  956. required=False,
  957. widget=StaticSelect2()
  958. )
  959. mgmt_only = forms.NullBooleanField(
  960. required=False,
  961. widget=BulkEditNullBooleanSelect,
  962. label='Management only'
  963. )
  964. class Meta:
  965. nullable_fields = []
  966. class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
  967. class Meta:
  968. model = FrontPortTemplate
  969. fields = [
  970. 'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
  971. ]
  972. widgets = {
  973. 'device_type': forms.HiddenInput(),
  974. 'rear_port': StaticSelect2(),
  975. }
  976. def __init__(self, *args, **kwargs):
  977. super().__init__(*args, **kwargs)
  978. # Limit rear_port choices to current DeviceType
  979. if hasattr(self.instance, 'device_type'):
  980. self.fields['rear_port'].queryset = RearPortTemplate.objects.filter(
  981. device_type=self.instance.device_type
  982. )
  983. class FrontPortTemplateCreateForm(ComponentForm):
  984. name_pattern = ExpandableNameField(
  985. label='Name'
  986. )
  987. type = forms.ChoiceField(
  988. choices=PORT_TYPE_CHOICES,
  989. widget=StaticSelect2()
  990. )
  991. rear_port_set = forms.MultipleChoiceField(
  992. choices=[],
  993. label='Rear ports',
  994. help_text='Select one rear port assignment for each front port being created.',
  995. )
  996. def __init__(self, *args, **kwargs):
  997. super().__init__(*args, **kwargs)
  998. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  999. occupied_port_positions = [
  1000. (front_port.rear_port_id, front_port.rear_port_position)
  1001. for front_port in self.parent.frontport_templates.all()
  1002. ]
  1003. # Populate rear port choices
  1004. choices = []
  1005. rear_ports = RearPortTemplate.objects.filter(device_type=self.parent)
  1006. for rear_port in rear_ports:
  1007. for i in range(1, rear_port.positions + 1):
  1008. if (rear_port.pk, i) not in occupied_port_positions:
  1009. choices.append(
  1010. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  1011. )
  1012. self.fields['rear_port_set'].choices = choices
  1013. def clean(self):
  1014. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  1015. front_port_count = len(self.cleaned_data['name_pattern'])
  1016. rear_port_count = len(self.cleaned_data['rear_port_set'])
  1017. if front_port_count != rear_port_count:
  1018. raise forms.ValidationError({
  1019. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  1020. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  1021. })
  1022. def get_iterative_data(self, iteration):
  1023. # Assign rear port and position from selected set
  1024. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  1025. return {
  1026. 'rear_port': int(rear_port),
  1027. 'rear_port_position': int(position),
  1028. }
  1029. class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
  1030. class Meta:
  1031. model = RearPortTemplate
  1032. fields = [
  1033. 'device_type', 'name', 'type', 'positions',
  1034. ]
  1035. widgets = {
  1036. 'device_type': forms.HiddenInput(),
  1037. 'type': StaticSelect2(),
  1038. }
  1039. class RearPortTemplateCreateForm(ComponentForm):
  1040. name_pattern = ExpandableNameField(
  1041. label='Name'
  1042. )
  1043. type = forms.ChoiceField(
  1044. choices=PORT_TYPE_CHOICES,
  1045. widget=StaticSelect2(),
  1046. )
  1047. positions = forms.IntegerField(
  1048. min_value=1,
  1049. max_value=64,
  1050. initial=1,
  1051. help_text='The number of front ports which may be mapped to each rear port'
  1052. )
  1053. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  1054. class Meta:
  1055. model = DeviceBayTemplate
  1056. fields = [
  1057. 'device_type', 'name',
  1058. ]
  1059. widgets = {
  1060. 'device_type': forms.HiddenInput(),
  1061. }
  1062. class DeviceBayTemplateCreateForm(ComponentForm):
  1063. name_pattern = ExpandableNameField(
  1064. label='Name'
  1065. )
  1066. #
  1067. # Component template import forms
  1068. #
  1069. class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
  1070. def __init__(self, device_type, data=None, *args, **kwargs):
  1071. # Must pass the parent DeviceType on form initialization
  1072. data.update({
  1073. 'device_type': device_type.pk,
  1074. })
  1075. super().__init__(data, *args, **kwargs)
  1076. def clean_device_type(self):
  1077. data = self.cleaned_data['device_type']
  1078. # Limit fields referencing other components to the parent DeviceType
  1079. for field_name, field in self.fields.items():
  1080. if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
  1081. field.queryset = field.queryset.filter(device_type=data)
  1082. return data
  1083. class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
  1084. type = forms.ChoiceField(
  1085. choices=ConsolePortTypes.TYPE_CHOICES
  1086. )
  1087. class Meta:
  1088. model = ConsolePortTemplate
  1089. fields = [
  1090. 'device_type', 'name', 'type',
  1091. ]
  1092. def clean_type(self):
  1093. # Convert slug value to field integer value
  1094. slug = self.cleaned_data['type']
  1095. return ConsolePortTypes.slug_to_integer(slug)
  1096. class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
  1097. type = forms.ChoiceField(
  1098. choices=ConsolePortTypes.TYPE_CHOICES
  1099. )
  1100. class Meta:
  1101. model = ConsoleServerPortTemplate
  1102. fields = [
  1103. 'device_type', 'name', 'type',
  1104. ]
  1105. def clean_type(self):
  1106. # Convert slug value to field integer value
  1107. slug = self.cleaned_data['type']
  1108. return ConsolePortTypes.slug_to_integer(slug)
  1109. class PowerPortTemplateImportForm(ComponentTemplateImportForm):
  1110. class Meta:
  1111. model = PowerPortTemplate
  1112. fields = [
  1113. 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
  1114. ]
  1115. class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
  1116. power_port = forms.ModelChoiceField(
  1117. queryset=PowerPortTemplate.objects.all(),
  1118. to_field_name='name',
  1119. required=False
  1120. )
  1121. class Meta:
  1122. model = PowerOutletTemplate
  1123. fields = [
  1124. 'device_type', 'name', 'type', 'power_port', 'feed_leg',
  1125. ]
  1126. class InterfaceTemplateImportForm(ComponentTemplateImportForm):
  1127. type = forms.ChoiceField(
  1128. choices=InterfaceTypes.TYPE_CHOICES
  1129. )
  1130. class Meta:
  1131. model = InterfaceTemplate
  1132. fields = [
  1133. 'device_type', 'name', 'type', 'mgmt_only',
  1134. ]
  1135. def clean_type(self):
  1136. # Convert slug value to field integer value
  1137. slug = self.cleaned_data['type']
  1138. return InterfaceTypes.slug_to_integer(slug)
  1139. class FrontPortTemplateImportForm(ComponentTemplateImportForm):
  1140. type = forms.ChoiceField(
  1141. choices=PortTypes.TYPE_CHOICES
  1142. )
  1143. rear_port = forms.ModelChoiceField(
  1144. queryset=RearPortTemplate.objects.all(),
  1145. to_field_name='name',
  1146. required=False
  1147. )
  1148. class Meta:
  1149. model = FrontPortTemplate
  1150. fields = [
  1151. 'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
  1152. ]
  1153. def clean_type(self):
  1154. # Convert slug value to field integer value
  1155. slug = self.cleaned_data['type']
  1156. return PortTypes.slug_to_integer(slug)
  1157. class RearPortTemplateImportForm(ComponentTemplateImportForm):
  1158. type = forms.ChoiceField(
  1159. choices=PortTypes.TYPE_CHOICES
  1160. )
  1161. class Meta:
  1162. model = RearPortTemplate
  1163. fields = [
  1164. 'device_type', 'name', 'type', 'positions',
  1165. ]
  1166. def clean_type(self):
  1167. # Convert slug value to field integer value
  1168. slug = self.cleaned_data['type']
  1169. return PortTypes.slug_to_integer(slug)
  1170. class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
  1171. class Meta:
  1172. model = DeviceBayTemplate
  1173. fields = [
  1174. 'device_type', 'name',
  1175. ]
  1176. #
  1177. # Device roles
  1178. #
  1179. class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
  1180. slug = SlugField()
  1181. class Meta:
  1182. model = DeviceRole
  1183. fields = [
  1184. 'name', 'slug', 'color', 'vm_role',
  1185. ]
  1186. class DeviceRoleCSVForm(forms.ModelForm):
  1187. slug = SlugField()
  1188. class Meta:
  1189. model = DeviceRole
  1190. fields = DeviceRole.csv_headers
  1191. help_texts = {
  1192. 'name': 'Name of device role',
  1193. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  1194. }
  1195. #
  1196. # Platforms
  1197. #
  1198. class PlatformForm(BootstrapMixin, forms.ModelForm):
  1199. slug = SlugField(
  1200. max_length=64
  1201. )
  1202. class Meta:
  1203. model = Platform
  1204. fields = [
  1205. 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args',
  1206. ]
  1207. widgets = {
  1208. 'manufacturer': APISelect(
  1209. api_url="/api/dcim/manufacturers/"
  1210. ),
  1211. 'napalm_args': SmallTextarea(),
  1212. }
  1213. class PlatformCSVForm(forms.ModelForm):
  1214. slug = SlugField()
  1215. manufacturer = forms.ModelChoiceField(
  1216. queryset=Manufacturer.objects.all(),
  1217. required=False,
  1218. to_field_name='name',
  1219. help_text='Manufacturer name',
  1220. error_messages={
  1221. 'invalid_choice': 'Manufacturer not found.',
  1222. }
  1223. )
  1224. class Meta:
  1225. model = Platform
  1226. fields = Platform.csv_headers
  1227. help_texts = {
  1228. 'name': 'Platform name',
  1229. }
  1230. #
  1231. # Devices
  1232. #
  1233. class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  1234. site = forms.ModelChoiceField(
  1235. queryset=Site.objects.all(),
  1236. widget=APISelect(
  1237. api_url="/api/dcim/sites/",
  1238. filter_for={
  1239. 'rack': 'site_id'
  1240. }
  1241. )
  1242. )
  1243. rack = ChainedModelChoiceField(
  1244. queryset=Rack.objects.all(),
  1245. chains=(
  1246. ('site', 'site'),
  1247. ),
  1248. required=False,
  1249. widget=APISelect(
  1250. api_url='/api/dcim/racks/',
  1251. display_field='display_name'
  1252. )
  1253. )
  1254. position = forms.TypedChoiceField(
  1255. required=False,
  1256. empty_value=None,
  1257. help_text="The lowest-numbered unit occupied by the device",
  1258. widget=APISelect(
  1259. api_url='/api/dcim/racks/{{rack}}/units/',
  1260. disabled_indicator='device'
  1261. )
  1262. )
  1263. manufacturer = forms.ModelChoiceField(
  1264. queryset=Manufacturer.objects.all(),
  1265. widget=APISelect(
  1266. api_url="/api/dcim/manufacturers/",
  1267. filter_for={
  1268. 'device_type': 'manufacturer_id'
  1269. }
  1270. )
  1271. )
  1272. device_type = ChainedModelChoiceField(
  1273. queryset=DeviceType.objects.all(),
  1274. chains=(
  1275. ('manufacturer', 'manufacturer'),
  1276. ),
  1277. label='Device type',
  1278. widget=APISelect(
  1279. api_url='/api/dcim/device-types/',
  1280. display_field='model'
  1281. )
  1282. )
  1283. cluster_group = forms.ModelChoiceField(
  1284. queryset=ClusterGroup.objects.all(),
  1285. required=False,
  1286. widget=APISelect(
  1287. api_url="/api/virtualization/cluster-groups/",
  1288. filter_for={
  1289. 'cluster': 'group_id'
  1290. },
  1291. attrs={
  1292. 'nullable': 'true'
  1293. }
  1294. )
  1295. )
  1296. cluster = ChainedModelChoiceField(
  1297. queryset=Cluster.objects.all(),
  1298. chains=(
  1299. ('group', 'cluster_group'),
  1300. ),
  1301. required=False,
  1302. widget=APISelect(
  1303. api_url='/api/virtualization/clusters/',
  1304. )
  1305. )
  1306. comments = CommentField()
  1307. tags = TagField(required=False)
  1308. local_context_data = JSONField(
  1309. required=False,
  1310. label=''
  1311. )
  1312. class Meta:
  1313. model = Device
  1314. fields = [
  1315. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face',
  1316. 'status', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group', 'cluster', 'tenant_group', 'tenant',
  1317. 'comments', 'tags', 'local_context_data'
  1318. ]
  1319. help_texts = {
  1320. 'device_role': "The function this device serves",
  1321. 'serial': "Chassis serial number",
  1322. 'local_context_data': "Local config context data overwrites all source contexts in the final rendered "
  1323. "config context",
  1324. }
  1325. widgets = {
  1326. 'face': StaticSelect2(
  1327. filter_for={
  1328. 'position': 'face'
  1329. }
  1330. ),
  1331. 'device_role': APISelect(
  1332. api_url='/api/dcim/device-roles/'
  1333. ),
  1334. 'status': StaticSelect2(),
  1335. 'platform': APISelect(
  1336. api_url="/api/dcim/platforms/"
  1337. ),
  1338. 'primary_ip4': StaticSelect2(),
  1339. 'primary_ip6': StaticSelect2(),
  1340. }
  1341. def __init__(self, *args, **kwargs):
  1342. # Initialize helper selectors
  1343. instance = kwargs.get('instance')
  1344. if 'initial' not in kwargs:
  1345. kwargs['initial'] = {}
  1346. # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
  1347. if instance and hasattr(instance, 'device_type'):
  1348. kwargs['initial']['manufacturer'] = instance.device_type.manufacturer
  1349. if instance and instance.cluster is not None:
  1350. kwargs['initial']['cluster_group'] = instance.cluster.group
  1351. super().__init__(*args, **kwargs)
  1352. if self.instance.pk:
  1353. # Compile list of choices for primary IPv4 and IPv6 addresses
  1354. for family in [4, 6]:
  1355. ip_choices = [(None, '---------')]
  1356. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  1357. interface_ids = self.instance.vc_interfaces.values('pk')
  1358. # Collect interface IPs
  1359. interface_ips = IPAddress.objects.prefetch_related('interface').filter(
  1360. family=family, interface_id__in=interface_ids
  1361. )
  1362. if interface_ips:
  1363. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  1364. ip_choices.append(('Interface IPs', ip_list))
  1365. # Collect NAT IPs
  1366. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  1367. family=family, nat_inside__interface__in=interface_ids
  1368. )
  1369. if nat_ips:
  1370. ip_list = [(ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips]
  1371. ip_choices.append(('NAT IPs', ip_list))
  1372. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  1373. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  1374. # can be flipped from one face to another.
  1375. self.fields['position'].widget.add_additional_query_param('exclude', self.instance.pk)
  1376. # Limit platform by manufacturer
  1377. self.fields['platform'].queryset = Platform.objects.filter(
  1378. Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer)
  1379. )
  1380. else:
  1381. # An object that doesn't exist yet can't have any IPs assigned to it
  1382. self.fields['primary_ip4'].choices = []
  1383. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  1384. self.fields['primary_ip6'].choices = []
  1385. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  1386. # Rack position
  1387. pk = self.instance.pk if self.instance.pk else None
  1388. try:
  1389. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  1390. position_choices = Rack.objects.get(pk=self.data['rack']) \
  1391. .get_rack_units(face=self.data.get('face'), exclude=pk)
  1392. elif self.initial.get('rack') and str(self.initial.get('face')):
  1393. position_choices = Rack.objects.get(pk=self.initial['rack']) \
  1394. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  1395. else:
  1396. position_choices = []
  1397. except Rack.DoesNotExist:
  1398. position_choices = []
  1399. self.fields['position'].choices = [('', '---------')] + [
  1400. (p['id'], {
  1401. 'label': p['name'],
  1402. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  1403. }) for p in position_choices
  1404. ]
  1405. # Disable rack assignment if this is a child device installed in a parent device
  1406. if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  1407. self.fields['site'].disabled = True
  1408. self.fields['rack'].disabled = True
  1409. self.initial['site'] = self.instance.parent_bay.device.site_id
  1410. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  1411. class BaseDeviceCSVForm(forms.ModelForm):
  1412. device_role = forms.ModelChoiceField(
  1413. queryset=DeviceRole.objects.all(),
  1414. to_field_name='name',
  1415. help_text='Name of assigned role',
  1416. error_messages={
  1417. 'invalid_choice': 'Invalid device role.',
  1418. }
  1419. )
  1420. tenant = forms.ModelChoiceField(
  1421. queryset=Tenant.objects.all(),
  1422. required=False,
  1423. to_field_name='name',
  1424. help_text='Name of assigned tenant',
  1425. error_messages={
  1426. 'invalid_choice': 'Tenant not found.',
  1427. }
  1428. )
  1429. manufacturer = forms.ModelChoiceField(
  1430. queryset=Manufacturer.objects.all(),
  1431. to_field_name='name',
  1432. help_text='Device type manufacturer',
  1433. error_messages={
  1434. 'invalid_choice': 'Invalid manufacturer.',
  1435. }
  1436. )
  1437. model_name = forms.CharField(
  1438. help_text='Device type model name'
  1439. )
  1440. platform = forms.ModelChoiceField(
  1441. queryset=Platform.objects.all(),
  1442. required=False,
  1443. to_field_name='name',
  1444. help_text='Name of assigned platform',
  1445. error_messages={
  1446. 'invalid_choice': 'Invalid platform.',
  1447. }
  1448. )
  1449. status = CSVChoiceField(
  1450. choices=DEVICE_STATUS_CHOICES,
  1451. help_text='Operational status'
  1452. )
  1453. class Meta:
  1454. fields = []
  1455. model = Device
  1456. help_texts = {
  1457. 'name': 'Device name',
  1458. }
  1459. def clean(self):
  1460. super().clean()
  1461. manufacturer = self.cleaned_data.get('manufacturer')
  1462. model_name = self.cleaned_data.get('model_name')
  1463. # Validate device type
  1464. if manufacturer and model_name:
  1465. try:
  1466. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  1467. except DeviceType.DoesNotExist:
  1468. raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
  1469. class DeviceCSVForm(BaseDeviceCSVForm):
  1470. site = forms.ModelChoiceField(
  1471. queryset=Site.objects.all(),
  1472. to_field_name='name',
  1473. help_text='Name of parent site',
  1474. error_messages={
  1475. 'invalid_choice': 'Invalid site name.',
  1476. }
  1477. )
  1478. rack_group = forms.CharField(
  1479. required=False,
  1480. help_text='Parent rack\'s group (if any)'
  1481. )
  1482. rack_name = forms.CharField(
  1483. required=False,
  1484. help_text='Name of parent rack'
  1485. )
  1486. face = CSVChoiceField(
  1487. choices=RACK_FACE_CHOICES,
  1488. required=False,
  1489. help_text='Mounted rack face'
  1490. )
  1491. cluster = forms.ModelChoiceField(
  1492. queryset=Cluster.objects.all(),
  1493. to_field_name='name',
  1494. required=False,
  1495. help_text='Virtualization cluster',
  1496. error_messages={
  1497. 'invalid_choice': 'Invalid cluster name.',
  1498. }
  1499. )
  1500. class Meta(BaseDeviceCSVForm.Meta):
  1501. fields = [
  1502. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  1503. 'site', 'rack_group', 'rack_name', 'position', 'face', 'cluster', 'comments',
  1504. ]
  1505. def clean(self):
  1506. super().clean()
  1507. site = self.cleaned_data.get('site')
  1508. rack_group = self.cleaned_data.get('rack_group')
  1509. rack_name = self.cleaned_data.get('rack_name')
  1510. # Validate rack
  1511. if site and rack_group and rack_name:
  1512. try:
  1513. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  1514. except Rack.DoesNotExist:
  1515. raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
  1516. elif site and rack_name:
  1517. try:
  1518. self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
  1519. except Rack.DoesNotExist:
  1520. raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
  1521. class ChildDeviceCSVForm(BaseDeviceCSVForm):
  1522. parent = FlexibleModelChoiceField(
  1523. queryset=Device.objects.all(),
  1524. to_field_name='name',
  1525. help_text='Name or ID of parent device',
  1526. error_messages={
  1527. 'invalid_choice': 'Parent device not found.',
  1528. }
  1529. )
  1530. device_bay_name = forms.CharField(
  1531. help_text='Name of device bay',
  1532. )
  1533. cluster = forms.ModelChoiceField(
  1534. queryset=Cluster.objects.all(),
  1535. to_field_name='name',
  1536. required=False,
  1537. help_text='Virtualization cluster',
  1538. error_messages={
  1539. 'invalid_choice': 'Invalid cluster name.',
  1540. }
  1541. )
  1542. class Meta(BaseDeviceCSVForm.Meta):
  1543. fields = [
  1544. 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
  1545. 'parent', 'device_bay_name', 'cluster', 'comments',
  1546. ]
  1547. def clean(self):
  1548. super().clean()
  1549. parent = self.cleaned_data.get('parent')
  1550. device_bay_name = self.cleaned_data.get('device_bay_name')
  1551. # Validate device bay
  1552. if parent and device_bay_name:
  1553. try:
  1554. self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
  1555. # Inherit site and rack from parent device
  1556. self.instance.site = parent.site
  1557. self.instance.rack = parent.rack
  1558. except DeviceBay.DoesNotExist:
  1559. raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
  1560. class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  1561. pk = forms.ModelMultipleChoiceField(
  1562. queryset=Device.objects.all(),
  1563. widget=forms.MultipleHiddenInput()
  1564. )
  1565. device_type = forms.ModelChoiceField(
  1566. queryset=DeviceType.objects.all(),
  1567. required=False,
  1568. label='Type',
  1569. widget=APISelect(
  1570. api_url="/api/dcim/device-types/",
  1571. display_field='display_name'
  1572. )
  1573. )
  1574. device_role = forms.ModelChoiceField(
  1575. queryset=DeviceRole.objects.all(),
  1576. required=False,
  1577. label='Role',
  1578. widget=APISelect(
  1579. api_url="/api/dcim/device-roles/"
  1580. )
  1581. )
  1582. tenant = forms.ModelChoiceField(
  1583. queryset=Tenant.objects.all(),
  1584. required=False,
  1585. widget=APISelect(
  1586. api_url="/api/tenancy/tenants/"
  1587. )
  1588. )
  1589. platform = forms.ModelChoiceField(
  1590. queryset=Platform.objects.all(),
  1591. required=False,
  1592. widget=APISelect(
  1593. api_url="/api/dcim/platforms/"
  1594. )
  1595. )
  1596. status = forms.ChoiceField(
  1597. choices=add_blank_choice(DEVICE_STATUS_CHOICES),
  1598. required=False,
  1599. initial='',
  1600. widget=StaticSelect2()
  1601. )
  1602. serial = forms.CharField(
  1603. max_length=50,
  1604. required=False,
  1605. label='Serial Number'
  1606. )
  1607. class Meta:
  1608. nullable_fields = [
  1609. 'tenant', 'platform', 'serial',
  1610. ]
  1611. class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm):
  1612. model = Device
  1613. field_order = [
  1614. 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant',
  1615. 'manufacturer_id', 'device_type_id', 'mac_address', 'has_primary_ip',
  1616. ]
  1617. q = forms.CharField(
  1618. required=False,
  1619. label='Search'
  1620. )
  1621. region = FilterChoiceField(
  1622. queryset=Region.objects.all(),
  1623. to_field_name='slug',
  1624. required=False,
  1625. widget=APISelectMultiple(
  1626. api_url="/api/dcim/regions/",
  1627. value_field="slug",
  1628. filter_for={
  1629. 'site': 'region'
  1630. }
  1631. )
  1632. )
  1633. site = FilterChoiceField(
  1634. queryset=Site.objects.all(),
  1635. to_field_name='slug',
  1636. widget=APISelectMultiple(
  1637. api_url="/api/dcim/sites/",
  1638. value_field="slug",
  1639. filter_for={
  1640. 'rack_group_id': 'site',
  1641. 'rack_id': 'site',
  1642. }
  1643. )
  1644. )
  1645. rack_group_id = FilterChoiceField(
  1646. queryset=RackGroup.objects.prefetch_related(
  1647. 'site'
  1648. ),
  1649. label='Rack group',
  1650. widget=APISelectMultiple(
  1651. api_url="/api/dcim/rack-groups/",
  1652. filter_for={
  1653. 'rack_id': 'rack_group_id',
  1654. }
  1655. )
  1656. )
  1657. rack_id = FilterChoiceField(
  1658. queryset=Rack.objects.all(),
  1659. label='Rack',
  1660. null_label='-- None --',
  1661. widget=APISelectMultiple(
  1662. api_url="/api/dcim/racks/",
  1663. null_option=True,
  1664. )
  1665. )
  1666. role = FilterChoiceField(
  1667. queryset=DeviceRole.objects.all(),
  1668. to_field_name='slug',
  1669. widget=APISelectMultiple(
  1670. api_url="/api/dcim/device-roles/",
  1671. value_field="slug",
  1672. )
  1673. )
  1674. manufacturer_id = FilterChoiceField(
  1675. queryset=Manufacturer.objects.all(),
  1676. label='Manufacturer',
  1677. widget=APISelectMultiple(
  1678. api_url="/api/dcim/manufacturers/",
  1679. filter_for={
  1680. 'device_type_id': 'manufacturer_id',
  1681. }
  1682. )
  1683. )
  1684. device_type_id = FilterChoiceField(
  1685. queryset=DeviceType.objects.prefetch_related(
  1686. 'manufacturer'
  1687. ),
  1688. label='Model',
  1689. widget=APISelectMultiple(
  1690. api_url="/api/dcim/device-types/",
  1691. display_field="model",
  1692. )
  1693. )
  1694. platform = FilterChoiceField(
  1695. queryset=Platform.objects.all(),
  1696. to_field_name='slug',
  1697. null_label='-- None --',
  1698. widget=APISelectMultiple(
  1699. api_url="/api/dcim/platforms/",
  1700. value_field="slug",
  1701. null_option=True,
  1702. )
  1703. )
  1704. status = forms.MultipleChoiceField(
  1705. choices=DEVICE_STATUS_CHOICES,
  1706. required=False,
  1707. widget=StaticSelect2Multiple()
  1708. )
  1709. mac_address = forms.CharField(
  1710. required=False,
  1711. label='MAC address'
  1712. )
  1713. has_primary_ip = forms.NullBooleanField(
  1714. required=False,
  1715. label='Has a primary IP',
  1716. widget=StaticSelect2(
  1717. choices=BOOLEAN_WITH_BLANK_CHOICES
  1718. )
  1719. )
  1720. virtual_chassis_member = forms.NullBooleanField(
  1721. required=False,
  1722. label='Virtual chassis member',
  1723. widget=StaticSelect2(
  1724. choices=BOOLEAN_WITH_BLANK_CHOICES
  1725. )
  1726. )
  1727. console_ports = forms.NullBooleanField(
  1728. required=False,
  1729. label='Has console ports',
  1730. widget=StaticSelect2(
  1731. choices=BOOLEAN_WITH_BLANK_CHOICES
  1732. )
  1733. )
  1734. console_server_ports = forms.NullBooleanField(
  1735. required=False,
  1736. label='Has console server ports',
  1737. widget=StaticSelect2(
  1738. choices=BOOLEAN_WITH_BLANK_CHOICES
  1739. )
  1740. )
  1741. power_ports = forms.NullBooleanField(
  1742. required=False,
  1743. label='Has power ports',
  1744. widget=StaticSelect2(
  1745. choices=BOOLEAN_WITH_BLANK_CHOICES
  1746. )
  1747. )
  1748. power_outlets = forms.NullBooleanField(
  1749. required=False,
  1750. label='Has power outlets',
  1751. widget=StaticSelect2(
  1752. choices=BOOLEAN_WITH_BLANK_CHOICES
  1753. )
  1754. )
  1755. interfaces = forms.NullBooleanField(
  1756. required=False,
  1757. label='Has interfaces',
  1758. widget=StaticSelect2(
  1759. choices=BOOLEAN_WITH_BLANK_CHOICES
  1760. )
  1761. )
  1762. pass_through_ports = forms.NullBooleanField(
  1763. required=False,
  1764. label='Has pass-through ports',
  1765. widget=StaticSelect2(
  1766. choices=BOOLEAN_WITH_BLANK_CHOICES
  1767. )
  1768. )
  1769. #
  1770. # Bulk device component creation
  1771. #
  1772. class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
  1773. pk = forms.ModelMultipleChoiceField(
  1774. queryset=Device.objects.all(),
  1775. widget=forms.MultipleHiddenInput()
  1776. )
  1777. name_pattern = ExpandableNameField(
  1778. label='Name'
  1779. )
  1780. class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
  1781. type = forms.ChoiceField(
  1782. choices=IFACE_TYPE_CHOICES,
  1783. widget=StaticSelect2()
  1784. )
  1785. enabled = forms.BooleanField(
  1786. required=False,
  1787. initial=True
  1788. )
  1789. mtu = forms.IntegerField(
  1790. required=False,
  1791. min_value=1,
  1792. max_value=32767,
  1793. label='MTU'
  1794. )
  1795. mgmt_only = forms.BooleanField(
  1796. required=False,
  1797. label='Management only'
  1798. )
  1799. description = forms.CharField(
  1800. max_length=100,
  1801. required=False
  1802. )
  1803. #
  1804. # Console ports
  1805. #
  1806. class ConsolePortForm(BootstrapMixin, forms.ModelForm):
  1807. tags = TagField(
  1808. required=False
  1809. )
  1810. class Meta:
  1811. model = ConsolePort
  1812. fields = [
  1813. 'device', 'name', 'type', 'description', 'tags',
  1814. ]
  1815. widgets = {
  1816. 'device': forms.HiddenInput(),
  1817. }
  1818. class ConsolePortCreateForm(ComponentForm):
  1819. name_pattern = ExpandableNameField(
  1820. label='Name'
  1821. )
  1822. type = forms.ChoiceField(
  1823. choices=CONSOLE_TYPE_CHOICES,
  1824. widget=StaticSelect2(),
  1825. )
  1826. description = forms.CharField(
  1827. max_length=100,
  1828. required=False
  1829. )
  1830. tags = TagField(
  1831. required=False
  1832. )
  1833. #
  1834. # Console server ports
  1835. #
  1836. class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
  1837. tags = TagField(
  1838. required=False
  1839. )
  1840. class Meta:
  1841. model = ConsoleServerPort
  1842. fields = [
  1843. 'device', 'name', 'type', 'description', 'tags',
  1844. ]
  1845. widgets = {
  1846. 'device': forms.HiddenInput(),
  1847. }
  1848. class ConsoleServerPortCreateForm(ComponentForm):
  1849. name_pattern = ExpandableNameField(
  1850. label='Name'
  1851. )
  1852. type = forms.ChoiceField(
  1853. choices=CONSOLE_TYPE_CHOICES,
  1854. widget=StaticSelect2(),
  1855. )
  1856. description = forms.CharField(
  1857. max_length=100,
  1858. required=False
  1859. )
  1860. tags = TagField(
  1861. required=False
  1862. )
  1863. class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  1864. pk = forms.ModelMultipleChoiceField(
  1865. queryset=ConsoleServerPort.objects.all(),
  1866. widget=forms.MultipleHiddenInput()
  1867. )
  1868. type = forms.ChoiceField(
  1869. choices=add_blank_choice(CONSOLE_TYPE_CHOICES),
  1870. required=False,
  1871. widget=StaticSelect2()
  1872. )
  1873. description = forms.CharField(
  1874. max_length=100,
  1875. required=False
  1876. )
  1877. class Meta:
  1878. nullable_fields = [
  1879. 'description',
  1880. ]
  1881. class ConsoleServerPortBulkRenameForm(BulkRenameForm):
  1882. pk = forms.ModelMultipleChoiceField(
  1883. queryset=ConsoleServerPort.objects.all(),
  1884. widget=forms.MultipleHiddenInput()
  1885. )
  1886. class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
  1887. pk = forms.ModelMultipleChoiceField(
  1888. queryset=ConsoleServerPort.objects.all(),
  1889. widget=forms.MultipleHiddenInput()
  1890. )
  1891. #
  1892. # Power ports
  1893. #
  1894. class PowerPortForm(BootstrapMixin, forms.ModelForm):
  1895. tags = TagField(
  1896. required=False
  1897. )
  1898. class Meta:
  1899. model = PowerPort
  1900. fields = [
  1901. 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
  1902. ]
  1903. widgets = {
  1904. 'device': forms.HiddenInput(),
  1905. }
  1906. class PowerPortCreateForm(ComponentForm):
  1907. name_pattern = ExpandableNameField(
  1908. label='Name'
  1909. )
  1910. type = forms.ChoiceField(
  1911. choices=add_blank_choice(PowerPortTypes.CHOICES),
  1912. required=False
  1913. )
  1914. maximum_draw = forms.IntegerField(
  1915. min_value=1,
  1916. required=False,
  1917. help_text="Maximum draw in watts"
  1918. )
  1919. allocated_draw = forms.IntegerField(
  1920. min_value=1,
  1921. required=False,
  1922. help_text="Allocated draw in watts"
  1923. )
  1924. description = forms.CharField(
  1925. max_length=100,
  1926. required=False
  1927. )
  1928. tags = TagField(
  1929. required=False
  1930. )
  1931. #
  1932. # Power outlets
  1933. #
  1934. class PowerOutletForm(BootstrapMixin, forms.ModelForm):
  1935. power_port = forms.ModelChoiceField(
  1936. queryset=PowerPort.objects.all(),
  1937. required=False
  1938. )
  1939. tags = TagField(
  1940. required=False
  1941. )
  1942. class Meta:
  1943. model = PowerOutlet
  1944. fields = [
  1945. 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags',
  1946. ]
  1947. widgets = {
  1948. 'device': forms.HiddenInput(),
  1949. }
  1950. def __init__(self, *args, **kwargs):
  1951. super().__init__(*args, **kwargs)
  1952. # Limit power_port choices to the local device
  1953. if hasattr(self.instance, 'device'):
  1954. self.fields['power_port'].queryset = PowerPort.objects.filter(
  1955. device=self.instance.device
  1956. )
  1957. class PowerOutletCreateForm(ComponentForm):
  1958. name_pattern = ExpandableNameField(
  1959. label='Name'
  1960. )
  1961. type = forms.ChoiceField(
  1962. choices=add_blank_choice(PowerOutletTypes.CHOICES),
  1963. required=False
  1964. )
  1965. power_port = forms.ModelChoiceField(
  1966. queryset=PowerPort.objects.all(),
  1967. required=False
  1968. )
  1969. feed_leg = forms.ChoiceField(
  1970. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  1971. required=False
  1972. )
  1973. description = forms.CharField(
  1974. max_length=100,
  1975. required=False
  1976. )
  1977. tags = TagField(
  1978. required=False
  1979. )
  1980. def __init__(self, *args, **kwargs):
  1981. super().__init__(*args, **kwargs)
  1982. # Limit power_port choices to those on the parent device
  1983. self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent)
  1984. class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  1985. pk = forms.ModelMultipleChoiceField(
  1986. queryset=PowerOutlet.objects.all(),
  1987. widget=forms.MultipleHiddenInput()
  1988. )
  1989. type = forms.ChoiceField(
  1990. choices=PowerOutletTypes.CHOICES,
  1991. required=False
  1992. )
  1993. feed_leg = forms.ChoiceField(
  1994. choices=add_blank_choice(POWERFEED_LEG_CHOICES),
  1995. required=False,
  1996. )
  1997. power_port = forms.ModelChoiceField(
  1998. queryset=PowerPort.objects.all(),
  1999. required=False
  2000. )
  2001. description = forms.CharField(
  2002. max_length=100,
  2003. required=False
  2004. )
  2005. class Meta:
  2006. nullable_fields = [
  2007. 'type', 'feed_leg', 'power_port', 'description',
  2008. ]
  2009. def __init__(self, *args, **kwargs):
  2010. super().__init__(*args, **kwargs)
  2011. # Limit power_port queryset to PowerPorts which belong to the parent Device
  2012. self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj)
  2013. class PowerOutletBulkRenameForm(BulkRenameForm):
  2014. pk = forms.ModelMultipleChoiceField(
  2015. queryset=PowerOutlet.objects.all(),
  2016. widget=forms.MultipleHiddenInput
  2017. )
  2018. class PowerOutletBulkDisconnectForm(ConfirmationForm):
  2019. pk = forms.ModelMultipleChoiceField(
  2020. queryset=PowerOutlet.objects.all(),
  2021. widget=forms.MultipleHiddenInput
  2022. )
  2023. #
  2024. # Interfaces
  2025. #
  2026. class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
  2027. untagged_vlan = forms.ModelChoiceField(
  2028. queryset=VLAN.objects.all(),
  2029. required=False,
  2030. widget=APISelect(
  2031. api_url="/api/ipam/vlans/",
  2032. display_field='display_name',
  2033. full=True
  2034. )
  2035. )
  2036. tagged_vlans = forms.ModelMultipleChoiceField(
  2037. queryset=VLAN.objects.all(),
  2038. required=False,
  2039. widget=APISelectMultiple(
  2040. api_url="/api/ipam/vlans/",
  2041. display_field='display_name',
  2042. full=True
  2043. )
  2044. )
  2045. tags = TagField(
  2046. required=False
  2047. )
  2048. class Meta:
  2049. model = Interface
  2050. fields = [
  2051. 'device', 'name', 'type', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description',
  2052. 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
  2053. ]
  2054. widgets = {
  2055. 'device': forms.HiddenInput(),
  2056. 'type': StaticSelect2(),
  2057. 'lag': StaticSelect2(),
  2058. 'mode': StaticSelect2(),
  2059. }
  2060. labels = {
  2061. 'mode': '802.1Q Mode',
  2062. }
  2063. help_texts = {
  2064. 'mode': INTERFACE_MODE_HELP_TEXT,
  2065. }
  2066. def __init__(self, *args, **kwargs):
  2067. super().__init__(*args, **kwargs)
  2068. # Limit LAG choices to interfaces belonging to this device (or VC master)
  2069. if self.is_bound:
  2070. device = Device.objects.get(pk=self.data['device'])
  2071. self.fields['lag'].queryset = Interface.objects.filter(
  2072. device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG
  2073. )
  2074. else:
  2075. device = self.instance.device
  2076. self.fields['lag'].queryset = Interface.objects.filter(
  2077. device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
  2078. )
  2079. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  2080. vlan_choices = []
  2081. global_vlans = VLAN.objects.filter(site=None, group=None)
  2082. vlan_choices.append(
  2083. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  2084. )
  2085. for group in VLANGroup.objects.filter(site=None):
  2086. global_group_vlans = VLAN.objects.filter(group=group)
  2087. vlan_choices.append(
  2088. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  2089. )
  2090. site = getattr(self.instance.parent, 'site', None)
  2091. if site is not None:
  2092. # Add non-grouped site VLANs
  2093. site_vlans = VLAN.objects.filter(site=site, group=None)
  2094. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  2095. # Add grouped site VLANs
  2096. for group in VLANGroup.objects.filter(site=site):
  2097. site_group_vlans = VLAN.objects.filter(group=group)
  2098. vlan_choices.append((
  2099. '{} / {}'.format(group.site.name, group.name),
  2100. [(vlan.pk, vlan) for vlan in site_group_vlans]
  2101. ))
  2102. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  2103. self.fields['tagged_vlans'].choices = vlan_choices
  2104. class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
  2105. name_pattern = ExpandableNameField(
  2106. label='Name'
  2107. )
  2108. type = forms.ChoiceField(
  2109. choices=IFACE_TYPE_CHOICES,
  2110. widget=StaticSelect2(),
  2111. )
  2112. enabled = forms.BooleanField(
  2113. required=False
  2114. )
  2115. lag = forms.ModelChoiceField(
  2116. queryset=Interface.objects.all(),
  2117. required=False,
  2118. label='Parent LAG',
  2119. widget=StaticSelect2(),
  2120. )
  2121. mtu = forms.IntegerField(
  2122. required=False,
  2123. min_value=1,
  2124. max_value=32767,
  2125. label='MTU'
  2126. )
  2127. mac_address = forms.CharField(
  2128. required=False,
  2129. label='MAC Address'
  2130. )
  2131. mgmt_only = forms.BooleanField(
  2132. required=False,
  2133. label='Management only',
  2134. help_text='This interface is used only for out-of-band management'
  2135. )
  2136. description = forms.CharField(
  2137. max_length=100,
  2138. required=False
  2139. )
  2140. mode = forms.ChoiceField(
  2141. choices=add_blank_choice(IFACE_MODE_CHOICES),
  2142. required=False,
  2143. widget=StaticSelect2(),
  2144. )
  2145. tags = TagField(
  2146. required=False
  2147. )
  2148. untagged_vlan = forms.ModelChoiceField(
  2149. queryset=VLAN.objects.all(),
  2150. required=False,
  2151. widget=APISelect(
  2152. api_url="/api/ipam/vlans/",
  2153. display_field='display_name',
  2154. full=True
  2155. )
  2156. )
  2157. tagged_vlans = forms.ModelMultipleChoiceField(
  2158. queryset=VLAN.objects.all(),
  2159. required=False,
  2160. widget=APISelectMultiple(
  2161. api_url="/api/ipam/vlans/",
  2162. display_field='display_name',
  2163. full=True
  2164. )
  2165. )
  2166. def __init__(self, *args, **kwargs):
  2167. # Set interfaces enabled by default
  2168. kwargs['initial'] = kwargs.get('initial', {}).copy()
  2169. kwargs['initial'].update({'enabled': True})
  2170. super().__init__(*args, **kwargs)
  2171. # Limit LAG choices to interfaces belonging to this device (or its VC master)
  2172. if self.parent is not None:
  2173. self.fields['lag'].queryset = Interface.objects.filter(
  2174. device__in=[self.parent, self.parent.get_vc_master()], type=IFACE_TYPE_LAG
  2175. )
  2176. else:
  2177. self.fields['lag'].queryset = Interface.objects.none()
  2178. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  2179. vlan_choices = []
  2180. global_vlans = VLAN.objects.filter(site=None, group=None)
  2181. vlan_choices.append(
  2182. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  2183. )
  2184. for group in VLANGroup.objects.filter(site=None):
  2185. global_group_vlans = VLAN.objects.filter(group=group)
  2186. vlan_choices.append(
  2187. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  2188. )
  2189. site = getattr(self.parent, 'site', None)
  2190. if site is not None:
  2191. # Add non-grouped site VLANs
  2192. site_vlans = VLAN.objects.filter(site=site, group=None)
  2193. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  2194. # Add grouped site VLANs
  2195. for group in VLANGroup.objects.filter(site=site):
  2196. site_group_vlans = VLAN.objects.filter(group=group)
  2197. vlan_choices.append((
  2198. '{} / {}'.format(group.site.name, group.name),
  2199. [(vlan.pk, vlan) for vlan in site_group_vlans]
  2200. ))
  2201. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  2202. self.fields['tagged_vlans'].choices = vlan_choices
  2203. class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2204. pk = forms.ModelMultipleChoiceField(
  2205. queryset=Interface.objects.all(),
  2206. widget=forms.MultipleHiddenInput()
  2207. )
  2208. type = forms.ChoiceField(
  2209. choices=add_blank_choice(IFACE_TYPE_CHOICES),
  2210. required=False,
  2211. widget=StaticSelect2()
  2212. )
  2213. enabled = forms.NullBooleanField(
  2214. required=False,
  2215. widget=BulkEditNullBooleanSelect()
  2216. )
  2217. lag = forms.ModelChoiceField(
  2218. queryset=Interface.objects.all(),
  2219. required=False,
  2220. label='Parent LAG',
  2221. widget=StaticSelect2()
  2222. )
  2223. mac_address = forms.CharField(
  2224. required=False,
  2225. label='MAC Address'
  2226. )
  2227. mtu = forms.IntegerField(
  2228. required=False,
  2229. min_value=1,
  2230. max_value=32767,
  2231. label='MTU'
  2232. )
  2233. mgmt_only = forms.NullBooleanField(
  2234. required=False,
  2235. widget=BulkEditNullBooleanSelect(),
  2236. label='Management only'
  2237. )
  2238. description = forms.CharField(
  2239. max_length=100,
  2240. required=False
  2241. )
  2242. mode = forms.ChoiceField(
  2243. choices=add_blank_choice(IFACE_MODE_CHOICES),
  2244. required=False,
  2245. widget=StaticSelect2()
  2246. )
  2247. untagged_vlan = forms.ModelChoiceField(
  2248. queryset=VLAN.objects.all(),
  2249. required=False,
  2250. widget=APISelect(
  2251. api_url="/api/ipam/vlans/",
  2252. display_field='display_name',
  2253. full=True
  2254. )
  2255. )
  2256. tagged_vlans = forms.ModelMultipleChoiceField(
  2257. queryset=VLAN.objects.all(),
  2258. required=False,
  2259. widget=APISelectMultiple(
  2260. api_url="/api/ipam/vlans/",
  2261. display_field='display_name',
  2262. full=True
  2263. )
  2264. )
  2265. class Meta:
  2266. nullable_fields = [
  2267. 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans'
  2268. ]
  2269. def __init__(self, *args, **kwargs):
  2270. super().__init__(*args, **kwargs)
  2271. # Limit LAG choices to interfaces which belong to the parent device (or VC master)
  2272. device = self.parent_obj
  2273. if device is not None:
  2274. self.fields['lag'].queryset = Interface.objects.filter(
  2275. device__in=[device, device.get_vc_master()],
  2276. type=IFACE_TYPE_LAG
  2277. )
  2278. else:
  2279. self.fields['lag'].choices = []
  2280. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  2281. vlan_choices = []
  2282. global_vlans = VLAN.objects.filter(site=None, group=None)
  2283. vlan_choices.append(
  2284. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  2285. )
  2286. for group in VLANGroup.objects.filter(site=None):
  2287. global_group_vlans = VLAN.objects.filter(group=group)
  2288. vlan_choices.append(
  2289. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  2290. )
  2291. if self.parent_obj is not None:
  2292. site = getattr(self.parent_obj, 'site', None)
  2293. if site is not None:
  2294. # Add non-grouped site VLANs
  2295. site_vlans = VLAN.objects.filter(site=site, group=None)
  2296. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  2297. # Add grouped site VLANs
  2298. for group in VLANGroup.objects.filter(site=site):
  2299. site_group_vlans = VLAN.objects.filter(group=group)
  2300. vlan_choices.append((
  2301. '{} / {}'.format(group.site.name, group.name),
  2302. [(vlan.pk, vlan) for vlan in site_group_vlans]
  2303. ))
  2304. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  2305. self.fields['tagged_vlans'].choices = vlan_choices
  2306. class InterfaceBulkRenameForm(BulkRenameForm):
  2307. pk = forms.ModelMultipleChoiceField(
  2308. queryset=Interface.objects.all(),
  2309. widget=forms.MultipleHiddenInput()
  2310. )
  2311. class InterfaceBulkDisconnectForm(ConfirmationForm):
  2312. pk = forms.ModelMultipleChoiceField(
  2313. queryset=Interface.objects.all(),
  2314. widget=forms.MultipleHiddenInput()
  2315. )
  2316. #
  2317. # Front pass-through ports
  2318. #
  2319. class FrontPortForm(BootstrapMixin, forms.ModelForm):
  2320. tags = TagField(
  2321. required=False
  2322. )
  2323. class Meta:
  2324. model = FrontPort
  2325. fields = [
  2326. 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'description', 'tags',
  2327. ]
  2328. widgets = {
  2329. 'device': forms.HiddenInput(),
  2330. 'type': StaticSelect2(),
  2331. 'rear_port': StaticSelect2(),
  2332. }
  2333. def __init__(self, *args, **kwargs):
  2334. super().__init__(*args, **kwargs)
  2335. # Limit RearPort choices to the local device
  2336. if hasattr(self.instance, 'device'):
  2337. self.fields['rear_port'].queryset = self.fields['rear_port'].queryset.filter(
  2338. device=self.instance.device
  2339. )
  2340. # TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
  2341. class FrontPortCreateForm(ComponentForm):
  2342. name_pattern = ExpandableNameField(
  2343. label='Name'
  2344. )
  2345. type = forms.ChoiceField(
  2346. choices=PORT_TYPE_CHOICES,
  2347. widget=StaticSelect2(),
  2348. )
  2349. rear_port_set = forms.MultipleChoiceField(
  2350. choices=[],
  2351. label='Rear ports',
  2352. help_text='Select one rear port assignment for each front port being created.',
  2353. )
  2354. description = forms.CharField(
  2355. required=False
  2356. )
  2357. def __init__(self, *args, **kwargs):
  2358. super().__init__(*args, **kwargs)
  2359. # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
  2360. occupied_port_positions = [
  2361. (front_port.rear_port_id, front_port.rear_port_position)
  2362. for front_port in self.parent.frontports.all()
  2363. ]
  2364. # Populate rear port choices
  2365. choices = []
  2366. rear_ports = RearPort.objects.filter(device=self.parent)
  2367. for rear_port in rear_ports:
  2368. for i in range(1, rear_port.positions + 1):
  2369. if (rear_port.pk, i) not in occupied_port_positions:
  2370. choices.append(
  2371. ('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
  2372. )
  2373. self.fields['rear_port_set'].choices = choices
  2374. def clean(self):
  2375. # Validate that the number of ports being created equals the number of selected (rear port, position) tuples
  2376. front_port_count = len(self.cleaned_data['name_pattern'])
  2377. rear_port_count = len(self.cleaned_data['rear_port_set'])
  2378. if front_port_count != rear_port_count:
  2379. raise forms.ValidationError({
  2380. 'rear_port_set': 'The provided name pattern will create {} ports, however {} rear port assignments '
  2381. 'were selected. These counts must match.'.format(front_port_count, rear_port_count)
  2382. })
  2383. def get_iterative_data(self, iteration):
  2384. # Assign rear port and position from selected set
  2385. rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
  2386. return {
  2387. 'rear_port': int(rear_port),
  2388. 'rear_port_position': int(position),
  2389. }
  2390. class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2391. pk = forms.ModelMultipleChoiceField(
  2392. queryset=FrontPort.objects.all(),
  2393. widget=forms.MultipleHiddenInput()
  2394. )
  2395. type = forms.ChoiceField(
  2396. choices=add_blank_choice(PORT_TYPE_CHOICES),
  2397. required=False,
  2398. widget=StaticSelect2()
  2399. )
  2400. description = forms.CharField(
  2401. max_length=100,
  2402. required=False
  2403. )
  2404. class Meta:
  2405. nullable_fields = [
  2406. 'description',
  2407. ]
  2408. class FrontPortBulkRenameForm(BulkRenameForm):
  2409. pk = forms.ModelMultipleChoiceField(
  2410. queryset=FrontPort.objects.all(),
  2411. widget=forms.MultipleHiddenInput
  2412. )
  2413. class FrontPortBulkDisconnectForm(ConfirmationForm):
  2414. pk = forms.ModelMultipleChoiceField(
  2415. queryset=FrontPort.objects.all(),
  2416. widget=forms.MultipleHiddenInput
  2417. )
  2418. #
  2419. # Rear pass-through ports
  2420. #
  2421. class RearPortForm(BootstrapMixin, forms.ModelForm):
  2422. tags = TagField(
  2423. required=False
  2424. )
  2425. class Meta:
  2426. model = RearPort
  2427. fields = [
  2428. 'device', 'name', 'type', 'positions', 'description', 'tags',
  2429. ]
  2430. widgets = {
  2431. 'device': forms.HiddenInput(),
  2432. 'type': StaticSelect2(),
  2433. }
  2434. class RearPortCreateForm(ComponentForm):
  2435. name_pattern = ExpandableNameField(
  2436. label='Name'
  2437. )
  2438. type = forms.ChoiceField(
  2439. choices=PORT_TYPE_CHOICES,
  2440. widget=StaticSelect2(),
  2441. )
  2442. positions = forms.IntegerField(
  2443. min_value=1,
  2444. max_value=64,
  2445. initial=1,
  2446. help_text='The number of front ports which may be mapped to each rear port'
  2447. )
  2448. description = forms.CharField(
  2449. required=False
  2450. )
  2451. class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  2452. pk = forms.ModelMultipleChoiceField(
  2453. queryset=RearPort.objects.all(),
  2454. widget=forms.MultipleHiddenInput()
  2455. )
  2456. type = forms.ChoiceField(
  2457. choices=add_blank_choice(PORT_TYPE_CHOICES),
  2458. required=False,
  2459. widget=StaticSelect2()
  2460. )
  2461. description = forms.CharField(
  2462. max_length=100,
  2463. required=False
  2464. )
  2465. class Meta:
  2466. nullable_fields = [
  2467. 'description',
  2468. ]
  2469. class RearPortBulkRenameForm(BulkRenameForm):
  2470. pk = forms.ModelMultipleChoiceField(
  2471. queryset=RearPort.objects.all(),
  2472. widget=forms.MultipleHiddenInput
  2473. )
  2474. class RearPortBulkDisconnectForm(ConfirmationForm):
  2475. pk = forms.ModelMultipleChoiceField(
  2476. queryset=RearPort.objects.all(),
  2477. widget=forms.MultipleHiddenInput
  2478. )
  2479. #
  2480. # Cables
  2481. #
  2482. class ConnectCableToDeviceForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2483. """
  2484. Base form for connecting a Cable to a Device component
  2485. """
  2486. termination_b_site = forms.ModelChoiceField(
  2487. queryset=Site.objects.all(),
  2488. label='Site',
  2489. required=False,
  2490. widget=APISelect(
  2491. api_url='/api/dcim/sites/',
  2492. filter_for={
  2493. 'termination_b_rack': 'site_id',
  2494. 'termination_b_device': 'site_id',
  2495. }
  2496. )
  2497. )
  2498. termination_b_rack = ChainedModelChoiceField(
  2499. queryset=Rack.objects.all(),
  2500. chains=(
  2501. ('site', 'termination_b_site'),
  2502. ),
  2503. label='Rack',
  2504. required=False,
  2505. widget=APISelect(
  2506. api_url='/api/dcim/racks/',
  2507. filter_for={
  2508. 'termination_b_device': 'rack_id',
  2509. },
  2510. attrs={
  2511. 'nullable': 'true',
  2512. }
  2513. )
  2514. )
  2515. termination_b_device = ChainedModelChoiceField(
  2516. queryset=Device.objects.all(),
  2517. chains=(
  2518. ('site', 'termination_b_site'),
  2519. ('rack', 'termination_b_rack'),
  2520. ),
  2521. label='Device',
  2522. required=False,
  2523. widget=APISelect(
  2524. api_url='/api/dcim/devices/',
  2525. display_field='display_name',
  2526. filter_for={
  2527. 'termination_b_id': 'device_id',
  2528. }
  2529. )
  2530. )
  2531. class Meta:
  2532. model = Cable
  2533. fields = [
  2534. 'termination_b_site', 'termination_b_rack', 'termination_b_device', 'termination_b_id', 'type', 'status',
  2535. 'label', 'color', 'length', 'length_unit',
  2536. ]
  2537. class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
  2538. termination_b_id = forms.IntegerField(
  2539. label='Name',
  2540. widget=APISelect(
  2541. api_url='/api/dcim/console-ports/',
  2542. disabled_indicator='cable',
  2543. )
  2544. )
  2545. class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
  2546. termination_b_id = forms.IntegerField(
  2547. label='Name',
  2548. widget=APISelect(
  2549. api_url='/api/dcim/console-server-ports/',
  2550. disabled_indicator='cable',
  2551. )
  2552. )
  2553. class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
  2554. termination_b_id = forms.IntegerField(
  2555. label='Name',
  2556. widget=APISelect(
  2557. api_url='/api/dcim/power-ports/',
  2558. disabled_indicator='cable',
  2559. )
  2560. )
  2561. class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
  2562. termination_b_id = forms.IntegerField(
  2563. label='Name',
  2564. widget=APISelect(
  2565. api_url='/api/dcim/power-outlets/',
  2566. disabled_indicator='cable',
  2567. )
  2568. )
  2569. class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
  2570. termination_b_id = forms.IntegerField(
  2571. label='Name',
  2572. widget=APISelect(
  2573. api_url='/api/dcim/interfaces/',
  2574. disabled_indicator='cable',
  2575. additional_query_params={
  2576. 'kind': 'physical',
  2577. }
  2578. )
  2579. )
  2580. class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
  2581. termination_b_id = forms.IntegerField(
  2582. label='Name',
  2583. widget=APISelect(
  2584. api_url='/api/dcim/front-ports/',
  2585. disabled_indicator='cable',
  2586. )
  2587. )
  2588. class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
  2589. termination_b_id = forms.IntegerField(
  2590. label='Name',
  2591. widget=APISelect(
  2592. api_url='/api/dcim/rear-ports/',
  2593. disabled_indicator='cable',
  2594. )
  2595. )
  2596. class ConnectCableToCircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2597. termination_b_provider = forms.ModelChoiceField(
  2598. queryset=Provider.objects.all(),
  2599. label='Provider',
  2600. widget=APISelect(
  2601. api_url='/api/circuits/providers/',
  2602. filter_for={
  2603. 'termination_b_circuit': 'provider_id',
  2604. }
  2605. )
  2606. )
  2607. termination_b_site = forms.ModelChoiceField(
  2608. queryset=Site.objects.all(),
  2609. label='Site',
  2610. required=False,
  2611. widget=APISelect(
  2612. api_url='/api/dcim/sites/',
  2613. filter_for={
  2614. 'termination_b_circuit': 'site_id',
  2615. }
  2616. )
  2617. )
  2618. termination_b_circuit = ChainedModelChoiceField(
  2619. queryset=Circuit.objects.all(),
  2620. chains=(
  2621. ('provider', 'termination_b_provider'),
  2622. ),
  2623. label='Circuit',
  2624. widget=APISelect(
  2625. api_url='/api/circuits/circuits/',
  2626. display_field='cid',
  2627. filter_for={
  2628. 'termination_b_id': 'circuit_id',
  2629. }
  2630. )
  2631. )
  2632. termination_b_id = forms.IntegerField(
  2633. label='Side',
  2634. widget=APISelect(
  2635. api_url='/api/circuits/circuit-terminations/',
  2636. disabled_indicator='cable',
  2637. display_field='term_side'
  2638. )
  2639. )
  2640. class Meta:
  2641. model = Cable
  2642. fields = [
  2643. 'termination_b_provider', 'termination_b_site', 'termination_b_circuit', 'termination_b_id', 'type',
  2644. 'status', 'label', 'color', 'length', 'length_unit',
  2645. ]
  2646. class ConnectCableToPowerFeedForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
  2647. termination_b_site = forms.ModelChoiceField(
  2648. queryset=Site.objects.all(),
  2649. label='Site',
  2650. widget=APISelect(
  2651. api_url='/api/dcim/sites/',
  2652. display_field='cid',
  2653. filter_for={
  2654. 'termination_b_rackgroup': 'site_id',
  2655. 'termination_b_powerpanel': 'site_id',
  2656. }
  2657. )
  2658. )
  2659. termination_b_rackgroup = ChainedModelChoiceField(
  2660. queryset=RackGroup.objects.all(),
  2661. label='Rack Group',
  2662. chains=(
  2663. ('site', 'termination_b_site'),
  2664. ),
  2665. required=False,
  2666. widget=APISelect(
  2667. api_url='/api/dcim/rack-groups/',
  2668. display_field='cid',
  2669. filter_for={
  2670. 'termination_b_powerpanel': 'rackgroup_id',
  2671. }
  2672. )
  2673. )
  2674. termination_b_powerpanel = ChainedModelChoiceField(
  2675. queryset=PowerPanel.objects.all(),
  2676. chains=(
  2677. ('site', 'termination_b_site'),
  2678. ('rack_group', 'termination_b_rackgroup'),
  2679. ),
  2680. label='Power Panel',
  2681. widget=APISelect(
  2682. api_url='/api/dcim/power-panels/',
  2683. filter_for={
  2684. 'termination_b_id': 'power_panel_id',
  2685. }
  2686. )
  2687. )
  2688. termination_b_id = forms.IntegerField(
  2689. label='Name',
  2690. widget=APISelect(
  2691. api_url='/api/dcim/power-feeds/',
  2692. )
  2693. )
  2694. class Meta:
  2695. model = Cable
  2696. fields = [
  2697. 'termination_b_rackgroup', 'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'label',
  2698. 'color', 'length', 'length_unit',
  2699. ]
  2700. class CableForm(BootstrapMixin, forms.ModelForm):
  2701. class Meta:
  2702. model = Cable
  2703. fields = [
  2704. 'type', 'status', 'label', 'color', 'length', 'length_unit',
  2705. ]
  2706. class CableCSVForm(forms.ModelForm):
  2707. # Termination A
  2708. side_a_device = FlexibleModelChoiceField(
  2709. queryset=Device.objects.all(),
  2710. to_field_name='name',
  2711. help_text='Side A device name or ID',
  2712. error_messages={
  2713. 'invalid_choice': 'Side A device not found',
  2714. }
  2715. )
  2716. side_a_type = forms.ModelChoiceField(
  2717. queryset=ContentType.objects.all(),
  2718. limit_choices_to={
  2719. 'model__in': CABLE_TERMINATION_TYPES,
  2720. },
  2721. to_field_name='model',
  2722. help_text='Side A type'
  2723. )
  2724. side_a_name = forms.CharField(
  2725. help_text='Side A component'
  2726. )
  2727. # Termination B
  2728. side_b_device = FlexibleModelChoiceField(
  2729. queryset=Device.objects.all(),
  2730. to_field_name='name',
  2731. help_text='Side B device name or ID',
  2732. error_messages={
  2733. 'invalid_choice': 'Side B device not found',
  2734. }
  2735. )
  2736. side_b_type = forms.ModelChoiceField(
  2737. queryset=ContentType.objects.all(),
  2738. limit_choices_to={
  2739. 'model__in': CABLE_TERMINATION_TYPES,
  2740. },
  2741. to_field_name='model',
  2742. help_text='Side B type'
  2743. )
  2744. side_b_name = forms.CharField(
  2745. help_text='Side B component'
  2746. )
  2747. # Cable attributes
  2748. status = CSVChoiceField(
  2749. choices=CONNECTION_STATUS_CHOICES,
  2750. required=False,
  2751. help_text='Connection status'
  2752. )
  2753. type = CSVChoiceField(
  2754. choices=CABLE_TYPE_CHOICES,
  2755. required=False,
  2756. help_text='Cable type'
  2757. )
  2758. length_unit = CSVChoiceField(
  2759. choices=CABLE_LENGTH_UNIT_CHOICES,
  2760. required=False,
  2761. help_text='Length unit'
  2762. )
  2763. class Meta:
  2764. model = Cable
  2765. fields = [
  2766. 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_device', 'side_b_type', 'side_b_name', 'type',
  2767. 'status', 'label', 'color', 'length', 'length_unit',
  2768. ]
  2769. help_texts = {
  2770. 'color': 'RGB color in hexadecimal (e.g. 00ff00)'
  2771. }
  2772. # TODO: Merge the clean() methods for either end
  2773. def clean_side_a_name(self):
  2774. device = self.cleaned_data.get('side_a_device')
  2775. content_type = self.cleaned_data.get('side_a_type')
  2776. name = self.cleaned_data.get('side_a_name')
  2777. if not device or not content_type or not name:
  2778. return None
  2779. model = content_type.model_class()
  2780. try:
  2781. termination_object = model.objects.get(
  2782. device=device,
  2783. name=name
  2784. )
  2785. if termination_object.cable is not None:
  2786. raise forms.ValidationError(
  2787. "Side A: {} {} is already connected".format(device, termination_object)
  2788. )
  2789. except ObjectDoesNotExist:
  2790. raise forms.ValidationError(
  2791. "A side termination not found: {} {}".format(device, name)
  2792. )
  2793. self.instance.termination_a = termination_object
  2794. return termination_object
  2795. def clean_side_b_name(self):
  2796. device = self.cleaned_data.get('side_b_device')
  2797. content_type = self.cleaned_data.get('side_b_type')
  2798. name = self.cleaned_data.get('side_b_name')
  2799. if not device or not content_type or not name:
  2800. return None
  2801. model = content_type.model_class()
  2802. try:
  2803. termination_object = model.objects.get(
  2804. device=device,
  2805. name=name
  2806. )
  2807. if termination_object.cable is not None:
  2808. raise forms.ValidationError(
  2809. "Side B: {} {} is already connected".format(device, termination_object)
  2810. )
  2811. except ObjectDoesNotExist:
  2812. raise forms.ValidationError(
  2813. "B side termination not found: {} {}".format(device, name)
  2814. )
  2815. self.instance.termination_b = termination_object
  2816. return termination_object
  2817. def clean_length_unit(self):
  2818. # Avoid trying to save as NULL
  2819. length_unit = self.cleaned_data.get('length_unit', None)
  2820. return length_unit if length_unit is not None else ''
  2821. class CableBulkEditForm(BootstrapMixin, BulkEditForm):
  2822. pk = forms.ModelMultipleChoiceField(
  2823. queryset=Cable.objects.all(),
  2824. widget=forms.MultipleHiddenInput
  2825. )
  2826. type = forms.ChoiceField(
  2827. choices=add_blank_choice(CABLE_TYPE_CHOICES),
  2828. required=False,
  2829. initial='',
  2830. widget=StaticSelect2()
  2831. )
  2832. status = forms.ChoiceField(
  2833. choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
  2834. required=False,
  2835. widget=StaticSelect2(),
  2836. initial=''
  2837. )
  2838. label = forms.CharField(
  2839. max_length=100,
  2840. required=False
  2841. )
  2842. color = forms.CharField(
  2843. max_length=6,
  2844. required=False,
  2845. widget=ColorSelect()
  2846. )
  2847. length = forms.IntegerField(
  2848. min_value=1,
  2849. required=False
  2850. )
  2851. length_unit = forms.ChoiceField(
  2852. choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES),
  2853. required=False,
  2854. initial='',
  2855. widget=StaticSelect2()
  2856. )
  2857. class Meta:
  2858. nullable_fields = [
  2859. 'type', 'status', 'label', 'color', 'length',
  2860. ]
  2861. def clean(self):
  2862. # Validate length/unit
  2863. length = self.cleaned_data.get('length')
  2864. length_unit = self.cleaned_data.get('length_unit')
  2865. if length and not length_unit:
  2866. raise forms.ValidationError({
  2867. 'length_unit': "Must specify a unit when setting length"
  2868. })
  2869. class CableFilterForm(BootstrapMixin, forms.Form):
  2870. model = Cable
  2871. q = forms.CharField(
  2872. required=False,
  2873. label='Search'
  2874. )
  2875. site = FilterChoiceField(
  2876. queryset=Site.objects.all(),
  2877. to_field_name='slug',
  2878. widget=APISelectMultiple(
  2879. api_url="/api/dcim/sites/",
  2880. value_field="slug",
  2881. filter_for={
  2882. 'rack_id': 'site',
  2883. }
  2884. )
  2885. )
  2886. rack_id = FilterChoiceField(
  2887. queryset=Rack.objects.all(),
  2888. label='Rack',
  2889. null_label='-- None --',
  2890. widget=APISelectMultiple(
  2891. api_url="/api/dcim/racks/",
  2892. null_option=True,
  2893. )
  2894. )
  2895. type = forms.MultipleChoiceField(
  2896. choices=add_blank_choice(CABLE_TYPE_CHOICES),
  2897. required=False,
  2898. widget=StaticSelect2()
  2899. )
  2900. status = forms.ChoiceField(
  2901. required=False,
  2902. choices=add_blank_choice(CONNECTION_STATUS_CHOICES),
  2903. widget=StaticSelect2()
  2904. )
  2905. color = forms.CharField(
  2906. max_length=6,
  2907. required=False,
  2908. widget=ColorSelect()
  2909. )
  2910. device = forms.CharField(
  2911. required=False,
  2912. label='Device name'
  2913. )
  2914. #
  2915. # Device bays
  2916. #
  2917. class DeviceBayForm(BootstrapMixin, forms.ModelForm):
  2918. tags = TagField(
  2919. required=False
  2920. )
  2921. class Meta:
  2922. model = DeviceBay
  2923. fields = [
  2924. 'device', 'name', 'description', 'tags',
  2925. ]
  2926. widgets = {
  2927. 'device': forms.HiddenInput(),
  2928. }
  2929. class DeviceBayCreateForm(ComponentForm):
  2930. name_pattern = ExpandableNameField(
  2931. label='Name'
  2932. )
  2933. tags = TagField(
  2934. required=False
  2935. )
  2936. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  2937. installed_device = forms.ModelChoiceField(
  2938. queryset=Device.objects.all(),
  2939. label='Child Device',
  2940. help_text="Child devices must first be created and assigned to the site/rack of the parent device.",
  2941. widget=StaticSelect2(),
  2942. )
  2943. def __init__(self, device_bay, *args, **kwargs):
  2944. super().__init__(*args, **kwargs)
  2945. self.fields['installed_device'].queryset = Device.objects.filter(
  2946. site=device_bay.device.site,
  2947. rack=device_bay.device.rack,
  2948. parent_bay__isnull=True,
  2949. device_type__u_height=0,
  2950. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
  2951. ).exclude(pk=device_bay.device.pk)
  2952. class DeviceBayBulkRenameForm(BulkRenameForm):
  2953. pk = forms.ModelMultipleChoiceField(
  2954. queryset=DeviceBay.objects.all(),
  2955. widget=forms.MultipleHiddenInput()
  2956. )
  2957. #
  2958. # Connections
  2959. #
  2960. class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
  2961. site = forms.ModelChoiceField(
  2962. queryset=Site.objects.all(),
  2963. required=False,
  2964. to_field_name='slug'
  2965. )
  2966. device = forms.CharField(
  2967. required=False,
  2968. label='Device name'
  2969. )
  2970. class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
  2971. site = forms.ModelChoiceField(
  2972. queryset=Site.objects.all(),
  2973. required=False,
  2974. to_field_name='slug'
  2975. )
  2976. device = forms.CharField(
  2977. required=False,
  2978. label='Device name'
  2979. )
  2980. class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
  2981. site = forms.ModelChoiceField(
  2982. queryset=Site.objects.all(),
  2983. required=False,
  2984. to_field_name='slug'
  2985. )
  2986. device = forms.CharField(
  2987. required=False,
  2988. label='Device name'
  2989. )
  2990. #
  2991. # Inventory items
  2992. #
  2993. class InventoryItemForm(BootstrapMixin, forms.ModelForm):
  2994. tags = TagField(
  2995. required=False
  2996. )
  2997. class Meta:
  2998. model = InventoryItem
  2999. fields = [
  3000. 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'tags',
  3001. ]
  3002. widgets = {
  3003. 'manufacturer': APISelect(
  3004. api_url="/api/dcim/manufacturers/"
  3005. )
  3006. }
  3007. class InventoryItemCSVForm(forms.ModelForm):
  3008. device = FlexibleModelChoiceField(
  3009. queryset=Device.objects.all(),
  3010. to_field_name='name',
  3011. help_text='Device name or ID',
  3012. error_messages={
  3013. 'invalid_choice': 'Device not found.',
  3014. }
  3015. )
  3016. manufacturer = forms.ModelChoiceField(
  3017. queryset=Manufacturer.objects.all(),
  3018. to_field_name='name',
  3019. required=False,
  3020. help_text='Manufacturer name',
  3021. error_messages={
  3022. 'invalid_choice': 'Invalid manufacturer.',
  3023. }
  3024. )
  3025. class Meta:
  3026. model = InventoryItem
  3027. fields = InventoryItem.csv_headers
  3028. class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
  3029. pk = forms.ModelMultipleChoiceField(
  3030. queryset=InventoryItem.objects.all(),
  3031. widget=forms.MultipleHiddenInput()
  3032. )
  3033. manufacturer = forms.ModelChoiceField(
  3034. queryset=Manufacturer.objects.all(),
  3035. required=False
  3036. )
  3037. part_id = forms.CharField(
  3038. max_length=50,
  3039. required=False,
  3040. label='Part ID'
  3041. )
  3042. description = forms.CharField(
  3043. max_length=100,
  3044. required=False
  3045. )
  3046. class Meta:
  3047. nullable_fields = [
  3048. 'manufacturer', 'part_id', 'description',
  3049. ]
  3050. class InventoryItemFilterForm(BootstrapMixin, forms.Form):
  3051. model = InventoryItem
  3052. q = forms.CharField(
  3053. required=False,
  3054. label='Search'
  3055. )
  3056. device = forms.CharField(
  3057. required=False,
  3058. label='Device name'
  3059. )
  3060. manufacturer = FilterChoiceField(
  3061. queryset=Manufacturer.objects.all(),
  3062. to_field_name='slug',
  3063. null_label='-- None --'
  3064. )
  3065. discovered = forms.NullBooleanField(
  3066. required=False,
  3067. widget=forms.Select(
  3068. choices=BOOLEAN_WITH_BLANK_CHOICES
  3069. )
  3070. )
  3071. #
  3072. # Virtual chassis
  3073. #
  3074. class DeviceSelectionForm(forms.Form):
  3075. pk = forms.ModelMultipleChoiceField(
  3076. queryset=Device.objects.all(),
  3077. widget=forms.MultipleHiddenInput()
  3078. )
  3079. class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
  3080. tags = TagField(
  3081. required=False
  3082. )
  3083. class Meta:
  3084. model = VirtualChassis
  3085. fields = [
  3086. 'master', 'domain', 'tags',
  3087. ]
  3088. widgets = {
  3089. 'master': SelectWithPK(),
  3090. }
  3091. class BaseVCMemberFormSet(forms.BaseModelFormSet):
  3092. def clean(self):
  3093. super().clean()
  3094. # Check for duplicate VC position values
  3095. vc_position_list = []
  3096. for form in self.forms:
  3097. vc_position = form.cleaned_data.get('vc_position')
  3098. if vc_position:
  3099. if vc_position in vc_position_list:
  3100. error_msg = 'A virtual chassis member already exists in position {}.'.format(vc_position)
  3101. form.add_error('vc_position', error_msg)
  3102. vc_position_list.append(vc_position)
  3103. class DeviceVCMembershipForm(forms.ModelForm):
  3104. class Meta:
  3105. model = Device
  3106. fields = [
  3107. 'vc_position', 'vc_priority',
  3108. ]
  3109. labels = {
  3110. 'vc_position': 'Position',
  3111. 'vc_priority': 'Priority',
  3112. }
  3113. def __init__(self, validate_vc_position=False, *args, **kwargs):
  3114. super().__init__(*args, **kwargs)
  3115. # Require VC position (only required when the Device is a VirtualChassis member)
  3116. self.fields['vc_position'].required = True
  3117. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  3118. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  3119. self.validate_vc_position = validate_vc_position
  3120. def clean_vc_position(self):
  3121. vc_position = self.cleaned_data['vc_position']
  3122. if self.validate_vc_position:
  3123. conflicting_members = Device.objects.filter(
  3124. virtual_chassis=self.instance.virtual_chassis,
  3125. vc_position=vc_position
  3126. )
  3127. if conflicting_members.exists():
  3128. raise forms.ValidationError(
  3129. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  3130. )
  3131. return vc_position
  3132. class VCMemberSelectForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
  3133. site = forms.ModelChoiceField(
  3134. queryset=Site.objects.all(),
  3135. label='Site',
  3136. required=False,
  3137. widget=APISelect(
  3138. api_url="/api/dcim/sites/",
  3139. filter_for={
  3140. 'rack': 'site_id',
  3141. 'device': 'site_id',
  3142. }
  3143. )
  3144. )
  3145. rack = ChainedModelChoiceField(
  3146. queryset=Rack.objects.all(),
  3147. chains=(
  3148. ('site', 'site'),
  3149. ),
  3150. label='Rack',
  3151. required=False,
  3152. widget=APISelect(
  3153. api_url='/api/dcim/racks/',
  3154. filter_for={
  3155. 'device': 'rack_id'
  3156. },
  3157. attrs={
  3158. 'nullable': 'true',
  3159. }
  3160. )
  3161. )
  3162. device = ChainedModelChoiceField(
  3163. queryset=Device.objects.filter(
  3164. virtual_chassis__isnull=True
  3165. ),
  3166. chains=(
  3167. ('site', 'site'),
  3168. ('rack', 'rack'),
  3169. ),
  3170. label='Device',
  3171. widget=APISelect(
  3172. api_url='/api/dcim/devices/',
  3173. display_field='display_name',
  3174. disabled_indicator='virtual_chassis'
  3175. )
  3176. )
  3177. def clean_device(self):
  3178. device = self.cleaned_data['device']
  3179. if device.virtual_chassis is not None:
  3180. raise forms.ValidationError(
  3181. "Device {} is already assigned to a virtual chassis.".format(device)
  3182. )
  3183. return device
  3184. class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3185. model = VirtualChassis
  3186. q = forms.CharField(
  3187. required=False,
  3188. label='Search'
  3189. )
  3190. site = FilterChoiceField(
  3191. queryset=Site.objects.all(),
  3192. to_field_name='slug',
  3193. widget=APISelectMultiple(
  3194. api_url="/api/dcim/sites/",
  3195. value_field="slug",
  3196. )
  3197. )
  3198. tenant_group = FilterChoiceField(
  3199. queryset=TenantGroup.objects.all(),
  3200. to_field_name='slug',
  3201. null_label='-- None --',
  3202. widget=APISelectMultiple(
  3203. api_url="/api/tenancy/tenant-groups/",
  3204. value_field="slug",
  3205. null_option=True,
  3206. filter_for={
  3207. 'tenant': 'group'
  3208. }
  3209. )
  3210. )
  3211. tenant = FilterChoiceField(
  3212. queryset=Tenant.objects.all(),
  3213. to_field_name='slug',
  3214. null_label='-- None --',
  3215. widget=APISelectMultiple(
  3216. api_url="/api/tenancy/tenants/",
  3217. value_field="slug",
  3218. null_option=True,
  3219. )
  3220. )
  3221. #
  3222. # Power panels
  3223. #
  3224. class PowerPanelForm(BootstrapMixin, forms.ModelForm):
  3225. rack_group = ChainedModelChoiceField(
  3226. queryset=RackGroup.objects.all(),
  3227. chains=(
  3228. ('site', 'site'),
  3229. ),
  3230. required=False,
  3231. widget=APISelect(
  3232. api_url='/api/dcim/rack-groups/',
  3233. )
  3234. )
  3235. class Meta:
  3236. model = PowerPanel
  3237. fields = [
  3238. 'site', 'rack_group', 'name',
  3239. ]
  3240. widgets = {
  3241. 'site': APISelect(
  3242. api_url="/api/dcim/sites/",
  3243. filter_for={
  3244. 'rack_group': 'site_id',
  3245. }
  3246. ),
  3247. }
  3248. class PowerPanelCSVForm(forms.ModelForm):
  3249. site = forms.ModelChoiceField(
  3250. queryset=Site.objects.all(),
  3251. to_field_name='name',
  3252. help_text='Name of parent site',
  3253. error_messages={
  3254. 'invalid_choice': 'Site not found.',
  3255. }
  3256. )
  3257. rack_group_name = forms.CharField(
  3258. required=False,
  3259. help_text="Rack group name (optional)"
  3260. )
  3261. class Meta:
  3262. model = PowerPanel
  3263. fields = PowerPanel.csv_headers
  3264. def clean(self):
  3265. super().clean()
  3266. site = self.cleaned_data.get('site')
  3267. rack_group_name = self.cleaned_data.get('rack_group_name')
  3268. # Validate rack group
  3269. if rack_group_name:
  3270. try:
  3271. self.instance.rack_group = RackGroup.objects.get(site=site, name=rack_group_name)
  3272. except RackGroup.DoesNotExist:
  3273. raise forms.ValidationError(
  3274. "Rack group {} not found in site {}".format(rack_group_name, site)
  3275. )
  3276. class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3277. model = PowerPanel
  3278. q = forms.CharField(
  3279. required=False,
  3280. label='Search'
  3281. )
  3282. site = FilterChoiceField(
  3283. queryset=Site.objects.all(),
  3284. to_field_name='slug',
  3285. widget=APISelectMultiple(
  3286. api_url="/api/dcim/sites/",
  3287. value_field="slug",
  3288. filter_for={
  3289. 'rack_group_id': 'site',
  3290. }
  3291. )
  3292. )
  3293. rack_group_id = FilterChoiceField(
  3294. queryset=RackGroup.objects.all(),
  3295. label='Rack group (ID)',
  3296. null_label='-- None --',
  3297. widget=APISelectMultiple(
  3298. api_url="/api/dcim/rack-groups/",
  3299. null_option=True,
  3300. )
  3301. )
  3302. #
  3303. # Power feeds
  3304. #
  3305. class PowerFeedForm(BootstrapMixin, CustomFieldForm):
  3306. site = ChainedModelChoiceField(
  3307. queryset=Site.objects.all(),
  3308. required=False,
  3309. widget=APISelect(
  3310. api_url='/api/dcim/sites/',
  3311. filter_for={
  3312. 'power_panel': 'site_id',
  3313. 'rack': 'site_id',
  3314. }
  3315. )
  3316. )
  3317. comments = CommentField()
  3318. tags = TagField(
  3319. required=False
  3320. )
  3321. class Meta:
  3322. model = PowerFeed
  3323. fields = [
  3324. 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
  3325. 'max_utilization', 'comments', 'tags',
  3326. ]
  3327. widgets = {
  3328. 'power_panel': APISelect(
  3329. api_url="/api/dcim/power-panels/"
  3330. ),
  3331. 'rack': APISelect(
  3332. api_url="/api/dcim/racks/"
  3333. ),
  3334. 'status': StaticSelect2(),
  3335. 'type': StaticSelect2(),
  3336. 'supply': StaticSelect2(),
  3337. 'phase': StaticSelect2(),
  3338. }
  3339. def __init__(self, *args, **kwargs):
  3340. super().__init__(*args, **kwargs)
  3341. # Initialize site field
  3342. if self.instance and hasattr(self.instance, 'power_panel'):
  3343. self.initial['site'] = self.instance.power_panel.site
  3344. class PowerFeedCSVForm(forms.ModelForm):
  3345. site = forms.ModelChoiceField(
  3346. queryset=Site.objects.all(),
  3347. to_field_name='name',
  3348. help_text='Name of parent site',
  3349. error_messages={
  3350. 'invalid_choice': 'Site not found.',
  3351. }
  3352. )
  3353. panel_name = forms.ModelChoiceField(
  3354. queryset=PowerPanel.objects.all(),
  3355. to_field_name='name',
  3356. help_text='Name of upstream power panel',
  3357. error_messages={
  3358. 'invalid_choice': 'Power panel not found.',
  3359. }
  3360. )
  3361. rack_group = forms.CharField(
  3362. required=False,
  3363. help_text="Rack group name (optional)"
  3364. )
  3365. rack_name = forms.CharField(
  3366. required=False,
  3367. help_text="Rack name (optional)"
  3368. )
  3369. status = CSVChoiceField(
  3370. choices=POWERFEED_STATUS_CHOICES,
  3371. required=False,
  3372. help_text='Operational status'
  3373. )
  3374. type = CSVChoiceField(
  3375. choices=POWERFEED_TYPE_CHOICES,
  3376. required=False,
  3377. help_text='Primary or redundant'
  3378. )
  3379. supply = CSVChoiceField(
  3380. choices=POWERFEED_SUPPLY_CHOICES,
  3381. required=False,
  3382. help_text='AC/DC'
  3383. )
  3384. phase = CSVChoiceField(
  3385. choices=POWERFEED_PHASE_CHOICES,
  3386. required=False,
  3387. help_text='Single or three-phase'
  3388. )
  3389. class Meta:
  3390. model = PowerFeed
  3391. fields = PowerFeed.csv_headers
  3392. def clean(self):
  3393. super().clean()
  3394. site = self.cleaned_data.get('site')
  3395. panel_name = self.cleaned_data.get('panel_name')
  3396. rack_group = self.cleaned_data.get('rack_group')
  3397. rack_name = self.cleaned_data.get('rack_name')
  3398. # Validate power panel
  3399. if panel_name:
  3400. try:
  3401. self.instance.power_panel = PowerPanel.objects.get(site=site, name=panel_name)
  3402. except Rack.DoesNotExist:
  3403. raise forms.ValidationError(
  3404. "Power panel {} not found in site {}".format(panel_name, site)
  3405. )
  3406. # Validate rack
  3407. if rack_name:
  3408. try:
  3409. self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
  3410. except Rack.DoesNotExist:
  3411. raise forms.ValidationError(
  3412. "Rack {} not found in site {}, group {}".format(rack_name, site, rack_group)
  3413. )
  3414. class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  3415. pk = forms.ModelMultipleChoiceField(
  3416. queryset=PowerFeed.objects.all(),
  3417. widget=forms.MultipleHiddenInput
  3418. )
  3419. powerpanel = forms.ModelChoiceField(
  3420. queryset=PowerPanel.objects.all(),
  3421. required=False,
  3422. widget=APISelect(
  3423. api_url="/api/dcim/power-panels/",
  3424. filter_for={
  3425. 'rackgroup': 'site_id',
  3426. }
  3427. )
  3428. )
  3429. rack = forms.ModelChoiceField(
  3430. queryset=Rack.objects.all(),
  3431. required=False,
  3432. widget=APISelect(
  3433. api_url="/api/dcim/racks",
  3434. )
  3435. )
  3436. status = forms.ChoiceField(
  3437. choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
  3438. required=False,
  3439. initial='',
  3440. widget=StaticSelect2()
  3441. )
  3442. type = forms.ChoiceField(
  3443. choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
  3444. required=False,
  3445. initial='',
  3446. widget=StaticSelect2()
  3447. )
  3448. supply = forms.ChoiceField(
  3449. choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
  3450. required=False,
  3451. initial='',
  3452. widget=StaticSelect2()
  3453. )
  3454. phase = forms.ChoiceField(
  3455. choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
  3456. required=False,
  3457. initial='',
  3458. widget=StaticSelect2()
  3459. )
  3460. voltage = forms.IntegerField(
  3461. required=False
  3462. )
  3463. amperage = forms.IntegerField(
  3464. required=False
  3465. )
  3466. max_utilization = forms.IntegerField(
  3467. required=False
  3468. )
  3469. comments = forms.CharField(
  3470. required=False
  3471. )
  3472. class Meta:
  3473. nullable_fields = [
  3474. 'rackgroup', 'comments',
  3475. ]
  3476. class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
  3477. model = PowerFeed
  3478. q = forms.CharField(
  3479. required=False,
  3480. label='Search'
  3481. )
  3482. site = FilterChoiceField(
  3483. queryset=Site.objects.all(),
  3484. to_field_name='slug',
  3485. widget=APISelectMultiple(
  3486. api_url="/api/dcim/sites/",
  3487. value_field="slug",
  3488. filter_for={
  3489. 'power_panel_id': 'site',
  3490. 'rack_id': 'site',
  3491. }
  3492. )
  3493. )
  3494. power_panel_id = FilterChoiceField(
  3495. queryset=PowerPanel.objects.all(),
  3496. label='Power panel',
  3497. null_label='-- None --',
  3498. widget=APISelectMultiple(
  3499. api_url="/api/dcim/power-panels/",
  3500. null_option=True,
  3501. )
  3502. )
  3503. rack_id = FilterChoiceField(
  3504. queryset=Rack.objects.all(),
  3505. label='Rack',
  3506. null_label='-- None --',
  3507. widget=APISelectMultiple(
  3508. api_url="/api/dcim/racks/",
  3509. null_option=True,
  3510. )
  3511. )
  3512. status = forms.MultipleChoiceField(
  3513. choices=POWERFEED_STATUS_CHOICES,
  3514. required=False,
  3515. widget=StaticSelect2Multiple()
  3516. )
  3517. type = forms.ChoiceField(
  3518. choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
  3519. required=False,
  3520. widget=StaticSelect2()
  3521. )
  3522. supply = forms.ChoiceField(
  3523. choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
  3524. required=False,
  3525. widget=StaticSelect2()
  3526. )
  3527. phase = forms.ChoiceField(
  3528. choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
  3529. required=False,
  3530. widget=StaticSelect2()
  3531. )
  3532. voltage = forms.IntegerField(
  3533. required=False
  3534. )
  3535. amperage = forms.IntegerField(
  3536. required=False
  3537. )
  3538. max_utilization = forms.IntegerField(
  3539. required=False
  3540. )