model_forms.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415
  1. from django import forms
  2. from django.contrib.auth.models import User
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.utils.translation import gettext as _
  5. from timezone_field import TimeZoneFormField
  6. from dcim.choices import *
  7. from dcim.constants import *
  8. from dcim.models import *
  9. from extras.models import ConfigTemplate
  10. from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
  11. from netbox.forms import NetBoxModelForm
  12. from tenancy.forms import TenancyForm
  13. from utilities.forms import BootstrapMixin, add_blank_choice
  14. from utilities.forms.fields import (
  15. CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
  16. NumericArrayField, SlugField,
  17. )
  18. from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
  19. from virtualization.models import Cluster
  20. from wireless.models import WirelessLAN, WirelessLANGroup
  21. from .common import InterfaceCommonForm, ModuleCommonForm
  22. __all__ = (
  23. 'CableForm',
  24. 'ConsolePortForm',
  25. 'ConsolePortTemplateForm',
  26. 'ConsoleServerPortForm',
  27. 'ConsoleServerPortTemplateForm',
  28. 'DeviceBayForm',
  29. 'DeviceBayTemplateForm',
  30. 'DeviceForm',
  31. 'DeviceRoleForm',
  32. 'DeviceTypeForm',
  33. 'DeviceVCMembershipForm',
  34. 'FrontPortForm',
  35. 'FrontPortTemplateForm',
  36. 'InterfaceForm',
  37. 'InterfaceTemplateForm',
  38. 'InventoryItemForm',
  39. 'InventoryItemRoleForm',
  40. 'InventoryItemTemplateForm',
  41. 'LocationForm',
  42. 'ManufacturerForm',
  43. 'ModuleForm',
  44. 'ModuleBayForm',
  45. 'ModuleBayTemplateForm',
  46. 'ModuleTypeForm',
  47. 'PlatformForm',
  48. 'PopulateDeviceBayForm',
  49. 'PowerFeedForm',
  50. 'PowerOutletForm',
  51. 'PowerOutletTemplateForm',
  52. 'PowerPanelForm',
  53. 'PowerPortForm',
  54. 'PowerPortTemplateForm',
  55. 'RackForm',
  56. 'RackReservationForm',
  57. 'RackRoleForm',
  58. 'RearPortForm',
  59. 'RearPortTemplateForm',
  60. 'RegionForm',
  61. 'SiteForm',
  62. 'SiteGroupForm',
  63. 'VCMemberSelectForm',
  64. 'VirtualChassisForm',
  65. 'VirtualDeviceContextForm'
  66. )
  67. class RegionForm(NetBoxModelForm):
  68. parent = DynamicModelChoiceField(
  69. queryset=Region.objects.all(),
  70. required=False
  71. )
  72. slug = SlugField()
  73. fieldsets = (
  74. ('Region', (
  75. 'parent', 'name', 'slug', 'description', 'tags',
  76. )),
  77. )
  78. class Meta:
  79. model = Region
  80. fields = (
  81. 'parent', 'name', 'slug', 'description', 'tags',
  82. )
  83. class SiteGroupForm(NetBoxModelForm):
  84. parent = DynamicModelChoiceField(
  85. queryset=SiteGroup.objects.all(),
  86. required=False
  87. )
  88. slug = SlugField()
  89. fieldsets = (
  90. ('Site Group', (
  91. 'parent', 'name', 'slug', 'description', 'tags',
  92. )),
  93. )
  94. class Meta:
  95. model = SiteGroup
  96. fields = (
  97. 'parent', 'name', 'slug', 'description', 'tags',
  98. )
  99. class SiteForm(TenancyForm, NetBoxModelForm):
  100. region = DynamicModelChoiceField(
  101. queryset=Region.objects.all(),
  102. required=False
  103. )
  104. group = DynamicModelChoiceField(
  105. queryset=SiteGroup.objects.all(),
  106. required=False
  107. )
  108. asns = DynamicModelMultipleChoiceField(
  109. queryset=ASN.objects.all(),
  110. label=_('ASNs'),
  111. required=False
  112. )
  113. slug = SlugField()
  114. time_zone = TimeZoneFormField(
  115. choices=add_blank_choice(TimeZoneFormField().choices),
  116. required=False
  117. )
  118. comments = CommentField()
  119. fieldsets = (
  120. ('Site', (
  121. 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
  122. )),
  123. ('Tenancy', ('tenant_group', 'tenant')),
  124. ('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')),
  125. )
  126. class Meta:
  127. model = Site
  128. fields = (
  129. 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
  130. 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags',
  131. )
  132. widgets = {
  133. 'physical_address': forms.Textarea(
  134. attrs={
  135. 'rows': 3,
  136. }
  137. ),
  138. 'shipping_address': forms.Textarea(
  139. attrs={
  140. 'rows': 3,
  141. }
  142. ),
  143. }
  144. class LocationForm(TenancyForm, NetBoxModelForm):
  145. site = DynamicModelChoiceField(
  146. queryset=Site.objects.all(),
  147. selector=True
  148. )
  149. parent = DynamicModelChoiceField(
  150. queryset=Location.objects.all(),
  151. required=False,
  152. query_params={
  153. 'site_id': '$site'
  154. }
  155. )
  156. slug = SlugField()
  157. fieldsets = (
  158. ('Location', ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
  159. ('Tenancy', ('tenant_group', 'tenant')),
  160. )
  161. class Meta:
  162. model = Location
  163. fields = (
  164. 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'tags',
  165. )
  166. class RackRoleForm(NetBoxModelForm):
  167. slug = SlugField()
  168. fieldsets = (
  169. ('Rack Role', (
  170. 'name', 'slug', 'color', 'description', 'tags',
  171. )),
  172. )
  173. class Meta:
  174. model = RackRole
  175. fields = [
  176. 'name', 'slug', 'color', 'description', 'tags',
  177. ]
  178. class RackForm(TenancyForm, NetBoxModelForm):
  179. site = DynamicModelChoiceField(
  180. queryset=Site.objects.all(),
  181. selector=True
  182. )
  183. location = DynamicModelChoiceField(
  184. queryset=Location.objects.all(),
  185. required=False,
  186. query_params={
  187. 'site_id': '$site'
  188. }
  189. )
  190. role = DynamicModelChoiceField(
  191. queryset=RackRole.objects.all(),
  192. required=False
  193. )
  194. comments = CommentField()
  195. class Meta:
  196. model = Rack
  197. fields = [
  198. 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
  199. 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
  200. 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
  201. ]
  202. class RackReservationForm(TenancyForm, NetBoxModelForm):
  203. rack = DynamicModelChoiceField(
  204. queryset=Rack.objects.all(),
  205. selector=True
  206. )
  207. units = NumericArrayField(
  208. base_field=forms.IntegerField(),
  209. help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
  210. )
  211. user = forms.ModelChoiceField(
  212. queryset=User.objects.order_by(
  213. 'username'
  214. )
  215. )
  216. comments = CommentField()
  217. fieldsets = (
  218. ('Reservation', ('rack', 'units', 'user', 'description', 'tags')),
  219. ('Tenancy', ('tenant_group', 'tenant')),
  220. )
  221. class Meta:
  222. model = RackReservation
  223. fields = [
  224. 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
  225. ]
  226. class ManufacturerForm(NetBoxModelForm):
  227. slug = SlugField()
  228. fieldsets = (
  229. ('Manufacturer', (
  230. 'name', 'slug', 'description', 'tags',
  231. )),
  232. )
  233. class Meta:
  234. model = Manufacturer
  235. fields = [
  236. 'name', 'slug', 'description', 'tags',
  237. ]
  238. class DeviceTypeForm(NetBoxModelForm):
  239. manufacturer = DynamicModelChoiceField(
  240. queryset=Manufacturer.objects.all()
  241. )
  242. default_platform = DynamicModelChoiceField(
  243. queryset=Platform.objects.all(),
  244. required=False
  245. )
  246. slug = SlugField(
  247. slug_source='model'
  248. )
  249. comments = CommentField()
  250. fieldsets = (
  251. ('Device Type', ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
  252. ('Chassis', (
  253. 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
  254. )),
  255. ('Images', ('front_image', 'rear_image')),
  256. )
  257. class Meta:
  258. model = DeviceType
  259. fields = [
  260. 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'is_full_depth',
  261. 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
  262. 'comments', 'tags',
  263. ]
  264. widgets = {
  265. 'front_image': ClearableFileInput(attrs={
  266. 'accept': DEVICETYPE_IMAGE_FORMATS
  267. }),
  268. 'rear_image': ClearableFileInput(attrs={
  269. 'accept': DEVICETYPE_IMAGE_FORMATS
  270. }),
  271. }
  272. class ModuleTypeForm(NetBoxModelForm):
  273. manufacturer = DynamicModelChoiceField(
  274. queryset=Manufacturer.objects.all()
  275. )
  276. comments = CommentField()
  277. fieldsets = (
  278. ('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')),
  279. ('Weight', ('weight', 'weight_unit'))
  280. )
  281. class Meta:
  282. model = ModuleType
  283. fields = [
  284. 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', 'comments', 'tags',
  285. ]
  286. class DeviceRoleForm(NetBoxModelForm):
  287. config_template = DynamicModelChoiceField(
  288. queryset=ConfigTemplate.objects.all(),
  289. required=False
  290. )
  291. slug = SlugField()
  292. fieldsets = (
  293. ('Device Role', (
  294. 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
  295. )),
  296. )
  297. class Meta:
  298. model = DeviceRole
  299. fields = [
  300. 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
  301. ]
  302. class PlatformForm(NetBoxModelForm):
  303. manufacturer = DynamicModelChoiceField(
  304. queryset=Manufacturer.objects.all(),
  305. required=False
  306. )
  307. config_template = DynamicModelChoiceField(
  308. queryset=ConfigTemplate.objects.all(),
  309. required=False
  310. )
  311. slug = SlugField(
  312. max_length=64
  313. )
  314. fieldsets = (
  315. ('Platform', ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
  316. )
  317. class Meta:
  318. model = Platform
  319. fields = [
  320. 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
  321. ]
  322. class DeviceForm(TenancyForm, NetBoxModelForm):
  323. site = DynamicModelChoiceField(
  324. queryset=Site.objects.all(),
  325. selector=True
  326. )
  327. location = DynamicModelChoiceField(
  328. queryset=Location.objects.all(),
  329. required=False,
  330. query_params={
  331. 'site_id': '$site'
  332. },
  333. initial_params={
  334. 'racks': '$rack'
  335. }
  336. )
  337. rack = DynamicModelChoiceField(
  338. queryset=Rack.objects.all(),
  339. required=False,
  340. query_params={
  341. 'site_id': '$site',
  342. 'location_id': '$location',
  343. }
  344. )
  345. position = forms.DecimalField(
  346. required=False,
  347. help_text=_("The lowest-numbered unit occupied by the device"),
  348. widget=APISelect(
  349. api_url='/api/dcim/racks/{{rack}}/elevation/',
  350. attrs={
  351. 'disabled-indicator': 'device',
  352. 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
  353. }
  354. )
  355. )
  356. device_type = DynamicModelChoiceField(
  357. queryset=DeviceType.objects.all(),
  358. selector=True
  359. )
  360. device_role = DynamicModelChoiceField(
  361. queryset=DeviceRole.objects.all()
  362. )
  363. platform = DynamicModelChoiceField(
  364. queryset=Platform.objects.all(),
  365. required=False
  366. )
  367. cluster = DynamicModelChoiceField(
  368. queryset=Cluster.objects.all(),
  369. required=False,
  370. selector=True
  371. )
  372. comments = CommentField()
  373. local_context_data = JSONField(
  374. required=False,
  375. label=''
  376. )
  377. virtual_chassis = DynamicModelChoiceField(
  378. queryset=VirtualChassis.objects.all(),
  379. required=False,
  380. selector=True
  381. )
  382. vc_position = forms.IntegerField(
  383. required=False,
  384. label=_('Position'),
  385. help_text=_("The position in the virtual chassis this device is identified by")
  386. )
  387. vc_priority = forms.IntegerField(
  388. required=False,
  389. label=_('Priority'),
  390. help_text=_("The priority of the device in the virtual chassis")
  391. )
  392. config_template = DynamicModelChoiceField(
  393. queryset=ConfigTemplate.objects.all(),
  394. required=False
  395. )
  396. class Meta:
  397. model = Device
  398. fields = [
  399. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
  400. 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster',
  401. 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
  402. 'comments', 'tags', 'local_context_data'
  403. ]
  404. def __init__(self, *args, **kwargs):
  405. super().__init__(*args, **kwargs)
  406. if self.instance.pk:
  407. # Compile list of choices for primary IPv4 and IPv6 addresses
  408. for family in [4, 6]:
  409. ip_choices = [(None, '---------')]
  410. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  411. interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
  412. # Collect interface IPs
  413. interface_ips = IPAddress.objects.filter(
  414. address__family=family,
  415. assigned_object_type=ContentType.objects.get_for_model(Interface),
  416. assigned_object_id__in=interface_ids
  417. ).prefetch_related('assigned_object')
  418. if interface_ips:
  419. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  420. ip_choices.append(('Interface IPs', ip_list))
  421. # Collect NAT IPs
  422. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  423. address__family=family,
  424. nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
  425. nat_inside__assigned_object_id__in=interface_ids
  426. ).prefetch_related('assigned_object')
  427. if nat_ips:
  428. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  429. ip_choices.append(('NAT IPs', ip_list))
  430. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  431. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  432. # can be flipped from one face to another.
  433. self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
  434. # Disable rack assignment if this is a child device installed in a parent device
  435. if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  436. self.fields['site'].disabled = True
  437. self.fields['rack'].disabled = True
  438. self.initial['site'] = self.instance.parent_bay.device.site_id
  439. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  440. else:
  441. # An object that doesn't exist yet can't have any IPs assigned to it
  442. self.fields['primary_ip4'].choices = []
  443. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  444. self.fields['primary_ip6'].choices = []
  445. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  446. # Rack position
  447. position = self.data.get('position') or self.initial.get('position')
  448. if position:
  449. self.fields['position'].widget.choices = [(position, f'U{position}')]
  450. class ModuleForm(ModuleCommonForm, NetBoxModelForm):
  451. device = DynamicModelChoiceField(
  452. queryset=Device.objects.all(),
  453. initial_params={
  454. 'modulebays': '$module_bay'
  455. }
  456. )
  457. module_bay = DynamicModelChoiceField(
  458. queryset=ModuleBay.objects.all(),
  459. query_params={
  460. 'device_id': '$device'
  461. }
  462. )
  463. module_type = DynamicModelChoiceField(
  464. queryset=ModuleType.objects.all(),
  465. selector=True
  466. )
  467. comments = CommentField()
  468. replicate_components = forms.BooleanField(
  469. required=False,
  470. initial=True,
  471. help_text=_("Automatically populate components associated with this module type")
  472. )
  473. adopt_components = forms.BooleanField(
  474. required=False,
  475. initial=False,
  476. help_text=_("Adopt already existing components")
  477. )
  478. fieldsets = (
  479. ('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
  480. ('Hardware', (
  481. 'serial', 'asset_tag', 'replicate_components', 'adopt_components',
  482. )),
  483. )
  484. class Meta:
  485. model = Module
  486. fields = [
  487. 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
  488. 'adopt_components', 'description', 'comments',
  489. ]
  490. def __init__(self, *args, **kwargs):
  491. super().__init__(*args, **kwargs)
  492. if self.instance.pk:
  493. self.fields['device'].disabled = True
  494. self.fields['replicate_components'].initial = False
  495. self.fields['replicate_components'].disabled = True
  496. self.fields['adopt_components'].initial = False
  497. self.fields['adopt_components'].disabled = True
  498. class CableForm(TenancyForm, NetBoxModelForm):
  499. comments = CommentField()
  500. class Meta:
  501. model = Cable
  502. fields = [
  503. 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
  504. 'comments', 'tags',
  505. ]
  506. error_messages = {
  507. 'length': {
  508. 'max_value': 'Maximum length is 32767 (any unit)'
  509. }
  510. }
  511. class PowerPanelForm(NetBoxModelForm):
  512. site = DynamicModelChoiceField(
  513. queryset=Site.objects.all(),
  514. selector=True
  515. )
  516. location = DynamicModelChoiceField(
  517. queryset=Location.objects.all(),
  518. required=False,
  519. query_params={
  520. 'site_id': '$site'
  521. }
  522. )
  523. comments = CommentField()
  524. fieldsets = (
  525. ('Power Panel', ('site', 'location', 'name', 'description', 'tags')),
  526. )
  527. class Meta:
  528. model = PowerPanel
  529. fields = [
  530. 'site', 'location', 'name', 'description', 'comments', 'tags',
  531. ]
  532. class PowerFeedForm(NetBoxModelForm):
  533. power_panel = DynamicModelChoiceField(
  534. queryset=PowerPanel.objects.all(),
  535. selector=True
  536. )
  537. rack = DynamicModelChoiceField(
  538. queryset=Rack.objects.all(),
  539. required=False,
  540. selector=True
  541. )
  542. comments = CommentField()
  543. fieldsets = (
  544. ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
  545. ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
  546. )
  547. class Meta:
  548. model = PowerFeed
  549. fields = [
  550. 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
  551. 'max_utilization', 'description', 'comments', 'tags',
  552. ]
  553. #
  554. # Virtual chassis
  555. #
  556. class VirtualChassisForm(NetBoxModelForm):
  557. master = forms.ModelChoiceField(
  558. queryset=Device.objects.all(),
  559. required=False,
  560. )
  561. comments = CommentField()
  562. class Meta:
  563. model = VirtualChassis
  564. fields = [
  565. 'name', 'domain', 'master', 'description', 'comments', 'tags',
  566. ]
  567. widgets = {
  568. 'master': SelectWithPK(),
  569. }
  570. def __init__(self, *args, **kwargs):
  571. super().__init__(*args, **kwargs)
  572. self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
  573. class DeviceVCMembershipForm(forms.ModelForm):
  574. class Meta:
  575. model = Device
  576. fields = [
  577. 'vc_position', 'vc_priority',
  578. ]
  579. labels = {
  580. 'vc_position': 'Position',
  581. 'vc_priority': 'Priority',
  582. }
  583. def __init__(self, validate_vc_position=False, *args, **kwargs):
  584. super().__init__(*args, **kwargs)
  585. # Require VC position (only required when the Device is a VirtualChassis member)
  586. self.fields['vc_position'].required = True
  587. # Add bootstrap classes to form elements.
  588. self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
  589. self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
  590. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  591. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  592. self.validate_vc_position = validate_vc_position
  593. def clean_vc_position(self):
  594. vc_position = self.cleaned_data['vc_position']
  595. if self.validate_vc_position:
  596. conflicting_members = Device.objects.filter(
  597. virtual_chassis=self.instance.virtual_chassis,
  598. vc_position=vc_position
  599. )
  600. if conflicting_members.exists():
  601. raise forms.ValidationError(
  602. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  603. )
  604. return vc_position
  605. class VCMemberSelectForm(BootstrapMixin, forms.Form):
  606. device = DynamicModelChoiceField(
  607. queryset=Device.objects.all(),
  608. query_params={
  609. 'virtual_chassis_id': 'null',
  610. },
  611. selector=True
  612. )
  613. def clean_device(self):
  614. device = self.cleaned_data['device']
  615. if device.virtual_chassis is not None:
  616. raise forms.ValidationError(
  617. f"Device {device} is already assigned to a virtual chassis."
  618. )
  619. return device
  620. #
  621. # Device component templates
  622. #
  623. class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
  624. device_type = DynamicModelChoiceField(
  625. queryset=DeviceType.objects.all()
  626. )
  627. def __init__(self, *args, **kwargs):
  628. super().__init__(*args, **kwargs)
  629. # Disable reassignment of DeviceType when editing an existing instance
  630. if self.instance.pk:
  631. self.fields['device_type'].disabled = True
  632. class ModularComponentTemplateForm(ComponentTemplateForm):
  633. device_type = DynamicModelChoiceField(
  634. queryset=DeviceType.objects.all().all(),
  635. required=False
  636. )
  637. module_type = DynamicModelChoiceField(
  638. queryset=ModuleType.objects.all(),
  639. required=False
  640. )
  641. def __init__(self, *args, **kwargs):
  642. super().__init__(*args, **kwargs)
  643. # Disable reassignment of ModuleType when editing an existing instance
  644. if self.instance.pk:
  645. self.fields['module_type'].disabled = True
  646. class ConsolePortTemplateForm(ModularComponentTemplateForm):
  647. fieldsets = (
  648. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
  649. )
  650. class Meta:
  651. model = ConsolePortTemplate
  652. fields = [
  653. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  654. ]
  655. class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
  656. fieldsets = (
  657. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
  658. )
  659. class Meta:
  660. model = ConsoleServerPortTemplate
  661. fields = [
  662. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  663. ]
  664. class PowerPortTemplateForm(ModularComponentTemplateForm):
  665. fieldsets = (
  666. (None, (
  667. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  668. )),
  669. )
  670. class Meta:
  671. model = PowerPortTemplate
  672. fields = [
  673. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  674. ]
  675. class PowerOutletTemplateForm(ModularComponentTemplateForm):
  676. power_port = DynamicModelChoiceField(
  677. queryset=PowerPortTemplate.objects.all(),
  678. required=False,
  679. query_params={
  680. 'devicetype_id': '$device_type',
  681. }
  682. )
  683. fieldsets = (
  684. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
  685. )
  686. class Meta:
  687. model = PowerOutletTemplate
  688. fields = [
  689. 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
  690. ]
  691. class InterfaceTemplateForm(ModularComponentTemplateForm):
  692. bridge = DynamicModelChoiceField(
  693. queryset=InterfaceTemplate.objects.all(),
  694. required=False,
  695. query_params={
  696. 'devicetype_id': '$device_type',
  697. 'moduletype_id': '$module_type',
  698. }
  699. )
  700. fieldsets = (
  701. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
  702. ('PoE', ('poe_mode', 'poe_type'))
  703. )
  704. class Meta:
  705. model = InterfaceTemplate
  706. fields = [
  707. 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge',
  708. ]
  709. class FrontPortTemplateForm(ModularComponentTemplateForm):
  710. rear_port = DynamicModelChoiceField(
  711. queryset=RearPortTemplate.objects.all(),
  712. required=False,
  713. query_params={
  714. 'devicetype_id': '$device_type',
  715. 'moduletype_id': '$module_type',
  716. }
  717. )
  718. fieldsets = (
  719. (None, (
  720. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  721. 'description',
  722. )),
  723. )
  724. class Meta:
  725. model = FrontPortTemplate
  726. fields = [
  727. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  728. 'description',
  729. ]
  730. class RearPortTemplateForm(ModularComponentTemplateForm):
  731. fieldsets = (
  732. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
  733. )
  734. class Meta:
  735. model = RearPortTemplate
  736. fields = [
  737. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
  738. ]
  739. class ModuleBayTemplateForm(ComponentTemplateForm):
  740. fieldsets = (
  741. (None, ('device_type', 'name', 'label', 'position', 'description')),
  742. )
  743. class Meta:
  744. model = ModuleBayTemplate
  745. fields = [
  746. 'device_type', 'name', 'label', 'position', 'description',
  747. ]
  748. class DeviceBayTemplateForm(ComponentTemplateForm):
  749. fieldsets = (
  750. (None, ('device_type', 'name', 'label', 'description')),
  751. )
  752. class Meta:
  753. model = DeviceBayTemplate
  754. fields = [
  755. 'device_type', 'name', 'label', 'description',
  756. ]
  757. class InventoryItemTemplateForm(ComponentTemplateForm):
  758. parent = DynamicModelChoiceField(
  759. queryset=InventoryItemTemplate.objects.all(),
  760. required=False,
  761. query_params={
  762. 'devicetype_id': '$device_type'
  763. }
  764. )
  765. role = DynamicModelChoiceField(
  766. queryset=InventoryItemRole.objects.all(),
  767. required=False
  768. )
  769. manufacturer = DynamicModelChoiceField(
  770. queryset=Manufacturer.objects.all(),
  771. required=False
  772. )
  773. component_type = ContentTypeChoiceField(
  774. queryset=ContentType.objects.all(),
  775. limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
  776. required=False,
  777. widget=forms.HiddenInput
  778. )
  779. component_id = forms.IntegerField(
  780. required=False,
  781. widget=forms.HiddenInput
  782. )
  783. fieldsets = (
  784. (None, (
  785. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  786. 'component_type', 'component_id',
  787. )),
  788. )
  789. class Meta:
  790. model = InventoryItemTemplate
  791. fields = [
  792. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  793. 'component_type', 'component_id',
  794. ]
  795. #
  796. # Device components
  797. #
  798. class DeviceComponentForm(NetBoxModelForm):
  799. device = DynamicModelChoiceField(
  800. queryset=Device.objects.all(),
  801. selector=True
  802. )
  803. def __init__(self, *args, **kwargs):
  804. super().__init__(*args, **kwargs)
  805. # Disable reassignment of Device when editing an existing instance
  806. if self.instance.pk:
  807. self.fields['device'].disabled = True
  808. class ModularDeviceComponentForm(DeviceComponentForm):
  809. module = DynamicModelChoiceField(
  810. queryset=Module.objects.all(),
  811. required=False,
  812. query_params={
  813. 'device_id': '$device',
  814. }
  815. )
  816. class ConsolePortForm(ModularDeviceComponentForm):
  817. fieldsets = (
  818. (None, (
  819. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  820. )),
  821. )
  822. class Meta:
  823. model = ConsolePort
  824. fields = [
  825. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  826. ]
  827. class ConsoleServerPortForm(ModularDeviceComponentForm):
  828. fieldsets = (
  829. (None, (
  830. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  831. )),
  832. )
  833. class Meta:
  834. model = ConsoleServerPort
  835. fields = [
  836. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  837. ]
  838. class PowerPortForm(ModularDeviceComponentForm):
  839. fieldsets = (
  840. (None, (
  841. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  842. 'description', 'tags',
  843. )),
  844. )
  845. class Meta:
  846. model = PowerPort
  847. fields = [
  848. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  849. 'description', 'tags',
  850. ]
  851. class PowerOutletForm(ModularDeviceComponentForm):
  852. power_port = DynamicModelChoiceField(
  853. queryset=PowerPort.objects.all(),
  854. required=False,
  855. query_params={
  856. 'device_id': '$device',
  857. }
  858. )
  859. fieldsets = (
  860. (None, (
  861. 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  862. 'tags',
  863. )),
  864. )
  865. class Meta:
  866. model = PowerOutlet
  867. fields = [
  868. 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  869. 'tags',
  870. ]
  871. class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
  872. vdcs = DynamicModelMultipleChoiceField(
  873. queryset=VirtualDeviceContext.objects.all(),
  874. required=False,
  875. label='Virtual Device Contexts',
  876. query_params={
  877. 'device_id': '$device',
  878. }
  879. )
  880. parent = DynamicModelChoiceField(
  881. queryset=Interface.objects.all(),
  882. required=False,
  883. label=_('Parent interface'),
  884. query_params={
  885. 'device_id': '$device',
  886. }
  887. )
  888. bridge = DynamicModelChoiceField(
  889. queryset=Interface.objects.all(),
  890. required=False,
  891. label=_('Bridged interface'),
  892. query_params={
  893. 'device_id': '$device',
  894. }
  895. )
  896. lag = DynamicModelChoiceField(
  897. queryset=Interface.objects.all(),
  898. required=False,
  899. label=_('LAG interface'),
  900. query_params={
  901. 'device_id': '$device',
  902. 'type': 'lag',
  903. }
  904. )
  905. wireless_lan_group = DynamicModelChoiceField(
  906. queryset=WirelessLANGroup.objects.all(),
  907. required=False,
  908. label=_('Wireless LAN group')
  909. )
  910. wireless_lans = DynamicModelMultipleChoiceField(
  911. queryset=WirelessLAN.objects.all(),
  912. required=False,
  913. label=_('Wireless LANs'),
  914. query_params={
  915. 'group_id': '$wireless_lan_group',
  916. }
  917. )
  918. vlan_group = DynamicModelChoiceField(
  919. queryset=VLANGroup.objects.all(),
  920. required=False,
  921. label=_('VLAN group')
  922. )
  923. untagged_vlan = DynamicModelChoiceField(
  924. queryset=VLAN.objects.all(),
  925. required=False,
  926. label=_('Untagged VLAN'),
  927. query_params={
  928. 'group_id': '$vlan_group',
  929. 'available_on_device': '$device',
  930. }
  931. )
  932. tagged_vlans = DynamicModelMultipleChoiceField(
  933. queryset=VLAN.objects.all(),
  934. required=False,
  935. label=_('Tagged VLANs'),
  936. query_params={
  937. 'group_id': '$vlan_group',
  938. 'available_on_device': '$device',
  939. }
  940. )
  941. vrf = DynamicModelChoiceField(
  942. queryset=VRF.objects.all(),
  943. required=False,
  944. label=_('VRF')
  945. )
  946. wwn = forms.CharField(
  947. empty_value=None,
  948. required=False,
  949. label=_('WWN')
  950. )
  951. fieldsets = (
  952. ('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
  953. ('Addressing', ('vrf', 'mac_address', 'wwn')),
  954. ('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
  955. ('Related Interfaces', ('parent', 'bridge', 'lag')),
  956. ('PoE', ('poe_mode', 'poe_type')),
  957. ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
  958. ('Wireless', (
  959. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
  960. )),
  961. )
  962. class Meta:
  963. model = Interface
  964. fields = [
  965. 'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
  966. 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
  967. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
  968. 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
  969. ]
  970. widgets = {
  971. 'speed': NumberWithOptions(
  972. options=InterfaceSpeedChoices
  973. ),
  974. 'mode': HTMXSelect(),
  975. }
  976. labels = {
  977. 'mode': '802.1Q Mode',
  978. }
  979. class FrontPortForm(ModularDeviceComponentForm):
  980. rear_port = DynamicModelChoiceField(
  981. queryset=RearPort.objects.all(),
  982. query_params={
  983. 'device_id': '$device',
  984. }
  985. )
  986. fieldsets = (
  987. (None, (
  988. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  989. 'description', 'tags',
  990. )),
  991. )
  992. class Meta:
  993. model = FrontPort
  994. fields = [
  995. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  996. 'description', 'tags',
  997. ]
  998. class RearPortForm(ModularDeviceComponentForm):
  999. fieldsets = (
  1000. (None, (
  1001. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1002. )),
  1003. )
  1004. class Meta:
  1005. model = RearPort
  1006. fields = [
  1007. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1008. ]
  1009. class ModuleBayForm(DeviceComponentForm):
  1010. fieldsets = (
  1011. (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
  1012. )
  1013. class Meta:
  1014. model = ModuleBay
  1015. fields = [
  1016. 'device', 'name', 'label', 'position', 'description', 'tags',
  1017. ]
  1018. class DeviceBayForm(DeviceComponentForm):
  1019. fieldsets = (
  1020. (None, ('device', 'name', 'label', 'description', 'tags',)),
  1021. )
  1022. class Meta:
  1023. model = DeviceBay
  1024. fields = [
  1025. 'device', 'name', 'label', 'description', 'tags',
  1026. ]
  1027. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  1028. installed_device = forms.ModelChoiceField(
  1029. queryset=Device.objects.all(),
  1030. label=_('Child Device'),
  1031. help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
  1032. )
  1033. def __init__(self, device_bay, *args, **kwargs):
  1034. super().__init__(*args, **kwargs)
  1035. self.fields['installed_device'].queryset = Device.objects.filter(
  1036. site=device_bay.device.site,
  1037. rack=device_bay.device.rack,
  1038. parent_bay__isnull=True,
  1039. device_type__u_height=0,
  1040. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  1041. ).exclude(pk=device_bay.device.pk)
  1042. class InventoryItemForm(DeviceComponentForm):
  1043. parent = DynamicModelChoiceField(
  1044. queryset=InventoryItem.objects.all(),
  1045. required=False,
  1046. query_params={
  1047. 'device_id': '$device'
  1048. }
  1049. )
  1050. role = DynamicModelChoiceField(
  1051. queryset=InventoryItemRole.objects.all(),
  1052. required=False
  1053. )
  1054. manufacturer = DynamicModelChoiceField(
  1055. queryset=Manufacturer.objects.all(),
  1056. required=False
  1057. )
  1058. # Assigned component selectors
  1059. consoleport = DynamicModelChoiceField(
  1060. queryset=ConsolePort.objects.all(),
  1061. required=False,
  1062. query_params={
  1063. 'device_id': '$device'
  1064. },
  1065. label=_('Console port')
  1066. )
  1067. consoleserverport = DynamicModelChoiceField(
  1068. queryset=ConsoleServerPort.objects.all(),
  1069. required=False,
  1070. query_params={
  1071. 'device_id': '$device'
  1072. },
  1073. label=_('Console server port')
  1074. )
  1075. frontport = DynamicModelChoiceField(
  1076. queryset=FrontPort.objects.all(),
  1077. required=False,
  1078. query_params={
  1079. 'device_id': '$device'
  1080. },
  1081. label=_('Front port')
  1082. )
  1083. interface = DynamicModelChoiceField(
  1084. queryset=Interface.objects.all(),
  1085. required=False,
  1086. query_params={
  1087. 'device_id': '$device'
  1088. },
  1089. label=_('Interface')
  1090. )
  1091. poweroutlet = DynamicModelChoiceField(
  1092. queryset=PowerOutlet.objects.all(),
  1093. required=False,
  1094. query_params={
  1095. 'device_id': '$device'
  1096. },
  1097. label=_('Power outlet')
  1098. )
  1099. powerport = DynamicModelChoiceField(
  1100. queryset=PowerPort.objects.all(),
  1101. required=False,
  1102. query_params={
  1103. 'device_id': '$device'
  1104. },
  1105. label=_('Power port')
  1106. )
  1107. rearport = DynamicModelChoiceField(
  1108. queryset=RearPort.objects.all(),
  1109. required=False,
  1110. query_params={
  1111. 'device_id': '$device'
  1112. },
  1113. label=_('Rear port')
  1114. )
  1115. fieldsets = (
  1116. ('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
  1117. ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
  1118. )
  1119. class Meta:
  1120. model = InventoryItem
  1121. fields = [
  1122. 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
  1123. 'description', 'tags',
  1124. ]
  1125. def __init__(self, *args, **kwargs):
  1126. instance = kwargs.get('instance')
  1127. initial = kwargs.get('initial', {}).copy()
  1128. component_type = initial.get('component_type')
  1129. component_id = initial.get('component_id')
  1130. # Used for picking the default active tab for component selection
  1131. self.no_component = True
  1132. if instance:
  1133. # When editing set the initial value for component selectin
  1134. for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
  1135. if type(instance.component) is component_model.model_class():
  1136. initial[component_model.model] = instance.component
  1137. self.no_component = False
  1138. break
  1139. elif component_type and component_id:
  1140. # When adding the InventoryItem from a component page
  1141. if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
  1142. if component := content_type.model_class().objects.filter(pk=component_id).first():
  1143. initial[content_type.model] = component
  1144. self.no_component = False
  1145. kwargs['initial'] = initial
  1146. super().__init__(*args, **kwargs)
  1147. # Specifically allow editing the device of IntentoryItems
  1148. if self.instance.pk:
  1149. self.fields['device'].disabled = False
  1150. def clean(self):
  1151. super().clean()
  1152. # Handle object assignment
  1153. selected_objects = [
  1154. field for field in (
  1155. 'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
  1156. ) if self.cleaned_data[field]
  1157. ]
  1158. if len(selected_objects) > 1:
  1159. raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
  1160. elif selected_objects:
  1161. self.instance.component = self.cleaned_data[selected_objects[0]]
  1162. else:
  1163. self.instance.component = None
  1164. # Device component roles
  1165. #
  1166. class InventoryItemRoleForm(NetBoxModelForm):
  1167. slug = SlugField()
  1168. fieldsets = (
  1169. ('Inventory Item Role', (
  1170. 'name', 'slug', 'color', 'description', 'tags',
  1171. )),
  1172. )
  1173. class Meta:
  1174. model = InventoryItemRole
  1175. fields = [
  1176. 'name', 'slug', 'color', 'description', 'tags',
  1177. ]
  1178. class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
  1179. device = DynamicModelChoiceField(
  1180. queryset=Device.objects.all(),
  1181. selector=True
  1182. )
  1183. primary_ip4 = DynamicModelChoiceField(
  1184. queryset=IPAddress.objects.all(),
  1185. label='Primary IPv4',
  1186. required=False,
  1187. query_params={
  1188. 'device_id': '$device',
  1189. 'family': '4',
  1190. }
  1191. )
  1192. primary_ip6 = DynamicModelChoiceField(
  1193. queryset=IPAddress.objects.all(),
  1194. label='Primary IPv6',
  1195. required=False,
  1196. query_params={
  1197. 'device_id': '$device',
  1198. 'family': '6',
  1199. }
  1200. )
  1201. fieldsets = (
  1202. ('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
  1203. ('Tenancy', ('tenant_group', 'tenant'))
  1204. )
  1205. class Meta:
  1206. model = VirtualDeviceContext
  1207. fields = [
  1208. 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
  1209. 'comments', 'tags'
  1210. ]