forms.py 61 KB

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