forms.py 57 KB

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