model_forms.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424
  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', (
  316. 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
  317. )),
  318. )
  319. class Meta:
  320. model = Platform
  321. fields = [
  322. 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
  323. ]
  324. widgets = {
  325. 'napalm_args': forms.Textarea(),
  326. }
  327. class DeviceForm(TenancyForm, NetBoxModelForm):
  328. site = DynamicModelChoiceField(
  329. queryset=Site.objects.all(),
  330. selector=True
  331. )
  332. location = DynamicModelChoiceField(
  333. queryset=Location.objects.all(),
  334. required=False,
  335. query_params={
  336. 'site_id': '$site'
  337. },
  338. initial_params={
  339. 'racks': '$rack'
  340. }
  341. )
  342. rack = DynamicModelChoiceField(
  343. queryset=Rack.objects.all(),
  344. required=False,
  345. query_params={
  346. 'site_id': '$site',
  347. 'location_id': '$location',
  348. }
  349. )
  350. position = forms.DecimalField(
  351. required=False,
  352. help_text=_("The lowest-numbered unit occupied by the device"),
  353. localize=True,
  354. widget=APISelect(
  355. api_url='/api/dcim/racks/{{rack}}/elevation/',
  356. attrs={
  357. 'disabled-indicator': 'device',
  358. 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
  359. },
  360. )
  361. )
  362. device_type = DynamicModelChoiceField(
  363. queryset=DeviceType.objects.all(),
  364. selector=True
  365. )
  366. device_role = DynamicModelChoiceField(
  367. queryset=DeviceRole.objects.all()
  368. )
  369. platform = DynamicModelChoiceField(
  370. queryset=Platform.objects.all(),
  371. required=False
  372. )
  373. cluster = DynamicModelChoiceField(
  374. queryset=Cluster.objects.all(),
  375. required=False,
  376. selector=True
  377. )
  378. comments = CommentField()
  379. local_context_data = JSONField(
  380. required=False,
  381. label=''
  382. )
  383. virtual_chassis = DynamicModelChoiceField(
  384. queryset=VirtualChassis.objects.all(),
  385. required=False,
  386. selector=True
  387. )
  388. vc_position = forms.IntegerField(
  389. required=False,
  390. label=_('Position'),
  391. help_text=_("The position in the virtual chassis this device is identified by")
  392. )
  393. vc_priority = forms.IntegerField(
  394. required=False,
  395. label=_('Priority'),
  396. help_text=_("The priority of the device in the virtual chassis")
  397. )
  398. config_template = DynamicModelChoiceField(
  399. queryset=ConfigTemplate.objects.all(),
  400. required=False
  401. )
  402. class Meta:
  403. model = Device
  404. fields = [
  405. 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
  406. 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant',
  407. 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags',
  408. 'local_context_data'
  409. ]
  410. def __init__(self, *args, **kwargs):
  411. super().__init__(*args, **kwargs)
  412. if self.instance.pk:
  413. # Compile list of choices for primary IPv4 and IPv6 addresses
  414. for family in [4, 6]:
  415. ip_choices = [(None, '---------')]
  416. # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
  417. interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
  418. # Collect interface IPs
  419. interface_ips = IPAddress.objects.filter(
  420. address__family=family,
  421. assigned_object_type=ContentType.objects.get_for_model(Interface),
  422. assigned_object_id__in=interface_ids
  423. ).prefetch_related('assigned_object')
  424. if interface_ips:
  425. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  426. ip_choices.append(('Interface IPs', ip_list))
  427. # Collect NAT IPs
  428. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  429. address__family=family,
  430. nat_inside__assigned_object_type=ContentType.objects.get_for_model(Interface),
  431. nat_inside__assigned_object_id__in=interface_ids
  432. ).prefetch_related('assigned_object')
  433. if nat_ips:
  434. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  435. ip_choices.append(('NAT IPs', ip_list))
  436. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  437. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  438. # can be flipped from one face to another.
  439. self.fields['position'].widget.add_query_param('exclude', self.instance.pk)
  440. # Disable rack assignment if this is a child device installed in a parent device
  441. if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  442. self.fields['site'].disabled = True
  443. self.fields['rack'].disabled = True
  444. self.initial['site'] = self.instance.parent_bay.device.site_id
  445. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  446. else:
  447. # An object that doesn't exist yet can't have any IPs assigned to it
  448. self.fields['primary_ip4'].choices = []
  449. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  450. self.fields['primary_ip6'].choices = []
  451. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  452. # Rack position
  453. position = self.data.get('position') or self.initial.get('position')
  454. if position:
  455. self.fields['position'].widget.choices = [(position, f'U{position}')]
  456. class ModuleForm(ModuleCommonForm, NetBoxModelForm):
  457. device = DynamicModelChoiceField(
  458. queryset=Device.objects.all(),
  459. initial_params={
  460. 'modulebays': '$module_bay'
  461. }
  462. )
  463. module_bay = DynamicModelChoiceField(
  464. queryset=ModuleBay.objects.all(),
  465. query_params={
  466. 'device_id': '$device'
  467. }
  468. )
  469. module_type = DynamicModelChoiceField(
  470. queryset=ModuleType.objects.all(),
  471. selector=True
  472. )
  473. comments = CommentField()
  474. replicate_components = forms.BooleanField(
  475. required=False,
  476. initial=True,
  477. help_text=_("Automatically populate components associated with this module type")
  478. )
  479. adopt_components = forms.BooleanField(
  480. required=False,
  481. initial=False,
  482. help_text=_("Adopt already existing components")
  483. )
  484. fieldsets = (
  485. ('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
  486. ('Hardware', (
  487. 'serial', 'asset_tag', 'replicate_components', 'adopt_components',
  488. )),
  489. )
  490. class Meta:
  491. model = Module
  492. fields = [
  493. 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
  494. 'adopt_components', 'description', 'comments',
  495. ]
  496. def __init__(self, *args, **kwargs):
  497. super().__init__(*args, **kwargs)
  498. if self.instance.pk:
  499. self.fields['device'].disabled = True
  500. self.fields['replicate_components'].initial = False
  501. self.fields['replicate_components'].disabled = True
  502. self.fields['adopt_components'].initial = False
  503. self.fields['adopt_components'].disabled = True
  504. class CableForm(TenancyForm, NetBoxModelForm):
  505. comments = CommentField()
  506. class Meta:
  507. model = Cable
  508. fields = [
  509. 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', 'length', 'length_unit', 'description',
  510. 'comments', 'tags',
  511. ]
  512. error_messages = {
  513. 'length': {
  514. 'max_value': 'Maximum length is 32767 (any unit)'
  515. }
  516. }
  517. class PowerPanelForm(NetBoxModelForm):
  518. site = DynamicModelChoiceField(
  519. queryset=Site.objects.all(),
  520. selector=True
  521. )
  522. location = DynamicModelChoiceField(
  523. queryset=Location.objects.all(),
  524. required=False,
  525. query_params={
  526. 'site_id': '$site'
  527. }
  528. )
  529. comments = CommentField()
  530. fieldsets = (
  531. ('Power Panel', ('site', 'location', 'name', 'description', 'tags')),
  532. )
  533. class Meta:
  534. model = PowerPanel
  535. fields = [
  536. 'site', 'location', 'name', 'description', 'comments', 'tags',
  537. ]
  538. class PowerFeedForm(NetBoxModelForm):
  539. power_panel = DynamicModelChoiceField(
  540. queryset=PowerPanel.objects.all(),
  541. selector=True
  542. )
  543. rack = DynamicModelChoiceField(
  544. queryset=Rack.objects.all(),
  545. required=False,
  546. selector=True
  547. )
  548. comments = CommentField()
  549. fieldsets = (
  550. ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
  551. ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
  552. )
  553. class Meta:
  554. model = PowerFeed
  555. fields = [
  556. 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
  557. 'max_utilization', 'description', 'comments', 'tags',
  558. ]
  559. #
  560. # Virtual chassis
  561. #
  562. class VirtualChassisForm(NetBoxModelForm):
  563. master = forms.ModelChoiceField(
  564. queryset=Device.objects.all(),
  565. required=False,
  566. )
  567. comments = CommentField()
  568. class Meta:
  569. model = VirtualChassis
  570. fields = [
  571. 'name', 'domain', 'master', 'description', 'comments', 'tags',
  572. ]
  573. widgets = {
  574. 'master': SelectWithPK(),
  575. }
  576. def __init__(self, *args, **kwargs):
  577. super().__init__(*args, **kwargs)
  578. self.fields['master'].queryset = Device.objects.filter(virtual_chassis=self.instance)
  579. class DeviceVCMembershipForm(forms.ModelForm):
  580. class Meta:
  581. model = Device
  582. fields = [
  583. 'vc_position', 'vc_priority',
  584. ]
  585. labels = {
  586. 'vc_position': 'Position',
  587. 'vc_priority': 'Priority',
  588. }
  589. def __init__(self, validate_vc_position=False, *args, **kwargs):
  590. super().__init__(*args, **kwargs)
  591. # Require VC position (only required when the Device is a VirtualChassis member)
  592. self.fields['vc_position'].required = True
  593. # Add bootstrap classes to form elements.
  594. self.fields['vc_position'].widget.attrs = {'class': 'form-control'}
  595. self.fields['vc_priority'].widget.attrs = {'class': 'form-control'}
  596. # Validation of vc_position is optional. This is only required when adding a new member to an existing
  597. # VirtualChassis. Otherwise, vc_position validation is handled by BaseVCMemberFormSet.
  598. self.validate_vc_position = validate_vc_position
  599. def clean_vc_position(self):
  600. vc_position = self.cleaned_data['vc_position']
  601. if self.validate_vc_position:
  602. conflicting_members = Device.objects.filter(
  603. virtual_chassis=self.instance.virtual_chassis,
  604. vc_position=vc_position
  605. )
  606. if conflicting_members.exists():
  607. raise forms.ValidationError(
  608. 'A virtual chassis member already exists in position {}.'.format(vc_position)
  609. )
  610. return vc_position
  611. class VCMemberSelectForm(BootstrapMixin, forms.Form):
  612. device = DynamicModelChoiceField(
  613. queryset=Device.objects.all(),
  614. query_params={
  615. 'virtual_chassis_id': 'null',
  616. },
  617. selector=True
  618. )
  619. def clean_device(self):
  620. device = self.cleaned_data['device']
  621. if device.virtual_chassis is not None:
  622. raise forms.ValidationError(
  623. f"Device {device} is already assigned to a virtual chassis."
  624. )
  625. return device
  626. #
  627. # Device component templates
  628. #
  629. class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
  630. device_type = DynamicModelChoiceField(
  631. queryset=DeviceType.objects.all()
  632. )
  633. def __init__(self, *args, **kwargs):
  634. super().__init__(*args, **kwargs)
  635. # Disable reassignment of DeviceType when editing an existing instance
  636. if self.instance.pk:
  637. self.fields['device_type'].disabled = True
  638. class ModularComponentTemplateForm(ComponentTemplateForm):
  639. device_type = DynamicModelChoiceField(
  640. queryset=DeviceType.objects.all().all(),
  641. required=False
  642. )
  643. module_type = DynamicModelChoiceField(
  644. queryset=ModuleType.objects.all(),
  645. required=False
  646. )
  647. def __init__(self, *args, **kwargs):
  648. super().__init__(*args, **kwargs)
  649. # Disable reassignment of ModuleType when editing an existing instance
  650. if self.instance.pk:
  651. self.fields['module_type'].disabled = True
  652. class ConsolePortTemplateForm(ModularComponentTemplateForm):
  653. fieldsets = (
  654. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
  655. )
  656. class Meta:
  657. model = ConsolePortTemplate
  658. fields = [
  659. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  660. ]
  661. class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
  662. fieldsets = (
  663. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
  664. )
  665. class Meta:
  666. model = ConsoleServerPortTemplate
  667. fields = [
  668. 'device_type', 'module_type', 'name', 'label', 'type', 'description',
  669. ]
  670. class PowerPortTemplateForm(ModularComponentTemplateForm):
  671. fieldsets = (
  672. (None, (
  673. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  674. )),
  675. )
  676. class Meta:
  677. model = PowerPortTemplate
  678. fields = [
  679. 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
  680. ]
  681. class PowerOutletTemplateForm(ModularComponentTemplateForm):
  682. power_port = DynamicModelChoiceField(
  683. queryset=PowerPortTemplate.objects.all(),
  684. required=False,
  685. query_params={
  686. 'devicetype_id': '$device_type',
  687. }
  688. )
  689. fieldsets = (
  690. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
  691. )
  692. class Meta:
  693. model = PowerOutletTemplate
  694. fields = [
  695. 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
  696. ]
  697. class InterfaceTemplateForm(ModularComponentTemplateForm):
  698. bridge = DynamicModelChoiceField(
  699. queryset=InterfaceTemplate.objects.all(),
  700. required=False,
  701. query_params={
  702. 'devicetype_id': '$device_type',
  703. 'moduletype_id': '$module_type',
  704. }
  705. )
  706. fieldsets = (
  707. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
  708. ('PoE', ('poe_mode', 'poe_type'))
  709. )
  710. class Meta:
  711. model = InterfaceTemplate
  712. fields = [
  713. 'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge',
  714. ]
  715. class FrontPortTemplateForm(ModularComponentTemplateForm):
  716. rear_port = DynamicModelChoiceField(
  717. queryset=RearPortTemplate.objects.all(),
  718. required=False,
  719. query_params={
  720. 'devicetype_id': '$device_type',
  721. 'moduletype_id': '$module_type',
  722. }
  723. )
  724. fieldsets = (
  725. (None, (
  726. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  727. 'description',
  728. )),
  729. )
  730. class Meta:
  731. model = FrontPortTemplate
  732. fields = [
  733. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
  734. 'description',
  735. ]
  736. class RearPortTemplateForm(ModularComponentTemplateForm):
  737. fieldsets = (
  738. (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
  739. )
  740. class Meta:
  741. model = RearPortTemplate
  742. fields = [
  743. 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
  744. ]
  745. class ModuleBayTemplateForm(ComponentTemplateForm):
  746. fieldsets = (
  747. (None, ('device_type', 'name', 'label', 'position', 'description')),
  748. )
  749. class Meta:
  750. model = ModuleBayTemplate
  751. fields = [
  752. 'device_type', 'name', 'label', 'position', 'description',
  753. ]
  754. class DeviceBayTemplateForm(ComponentTemplateForm):
  755. fieldsets = (
  756. (None, ('device_type', 'name', 'label', 'description')),
  757. )
  758. class Meta:
  759. model = DeviceBayTemplate
  760. fields = [
  761. 'device_type', 'name', 'label', 'description',
  762. ]
  763. class InventoryItemTemplateForm(ComponentTemplateForm):
  764. parent = DynamicModelChoiceField(
  765. queryset=InventoryItemTemplate.objects.all(),
  766. required=False,
  767. query_params={
  768. 'devicetype_id': '$device_type'
  769. }
  770. )
  771. role = DynamicModelChoiceField(
  772. queryset=InventoryItemRole.objects.all(),
  773. required=False
  774. )
  775. manufacturer = DynamicModelChoiceField(
  776. queryset=Manufacturer.objects.all(),
  777. required=False
  778. )
  779. component_type = ContentTypeChoiceField(
  780. queryset=ContentType.objects.all(),
  781. limit_choices_to=MODULAR_COMPONENT_TEMPLATE_MODELS,
  782. required=False,
  783. widget=forms.HiddenInput
  784. )
  785. component_id = forms.IntegerField(
  786. required=False,
  787. widget=forms.HiddenInput
  788. )
  789. fieldsets = (
  790. (None, (
  791. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  792. 'component_type', 'component_id',
  793. )),
  794. )
  795. class Meta:
  796. model = InventoryItemTemplate
  797. fields = [
  798. 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
  799. 'component_type', 'component_id',
  800. ]
  801. #
  802. # Device components
  803. #
  804. class DeviceComponentForm(NetBoxModelForm):
  805. device = DynamicModelChoiceField(
  806. queryset=Device.objects.all(),
  807. selector=True
  808. )
  809. def __init__(self, *args, **kwargs):
  810. super().__init__(*args, **kwargs)
  811. # Disable reassignment of Device when editing an existing instance
  812. if self.instance.pk:
  813. self.fields['device'].disabled = True
  814. class ModularDeviceComponentForm(DeviceComponentForm):
  815. module = DynamicModelChoiceField(
  816. queryset=Module.objects.all(),
  817. required=False,
  818. query_params={
  819. 'device_id': '$device',
  820. }
  821. )
  822. class ConsolePortForm(ModularDeviceComponentForm):
  823. fieldsets = (
  824. (None, (
  825. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  826. )),
  827. )
  828. class Meta:
  829. model = ConsolePort
  830. fields = [
  831. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  832. ]
  833. class ConsoleServerPortForm(ModularDeviceComponentForm):
  834. fieldsets = (
  835. (None, (
  836. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  837. )),
  838. )
  839. class Meta:
  840. model = ConsoleServerPort
  841. fields = [
  842. 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
  843. ]
  844. class PowerPortForm(ModularDeviceComponentForm):
  845. fieldsets = (
  846. (None, (
  847. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  848. 'description', 'tags',
  849. )),
  850. )
  851. class Meta:
  852. model = PowerPort
  853. fields = [
  854. 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
  855. 'description', 'tags',
  856. ]
  857. class PowerOutletForm(ModularDeviceComponentForm):
  858. power_port = DynamicModelChoiceField(
  859. queryset=PowerPort.objects.all(),
  860. required=False,
  861. query_params={
  862. 'device_id': '$device',
  863. }
  864. )
  865. fieldsets = (
  866. (None, (
  867. 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  868. 'tags',
  869. )),
  870. )
  871. class Meta:
  872. model = PowerOutlet
  873. fields = [
  874. 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
  875. 'tags',
  876. ]
  877. class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
  878. vdcs = DynamicModelMultipleChoiceField(
  879. queryset=VirtualDeviceContext.objects.all(),
  880. required=False,
  881. label='Virtual Device Contexts',
  882. initial_params={
  883. 'interfaces': '$parent',
  884. },
  885. query_params={
  886. 'device_id': '$device',
  887. }
  888. )
  889. parent = DynamicModelChoiceField(
  890. queryset=Interface.objects.all(),
  891. required=False,
  892. label=_('Parent interface'),
  893. query_params={
  894. 'device_id': '$device',
  895. }
  896. )
  897. bridge = DynamicModelChoiceField(
  898. queryset=Interface.objects.all(),
  899. required=False,
  900. label=_('Bridged interface'),
  901. query_params={
  902. 'device_id': '$device',
  903. }
  904. )
  905. lag = DynamicModelChoiceField(
  906. queryset=Interface.objects.all(),
  907. required=False,
  908. label=_('LAG interface'),
  909. query_params={
  910. 'device_id': '$device',
  911. 'type': 'lag',
  912. }
  913. )
  914. wireless_lan_group = DynamicModelChoiceField(
  915. queryset=WirelessLANGroup.objects.all(),
  916. required=False,
  917. label=_('Wireless LAN group')
  918. )
  919. wireless_lans = DynamicModelMultipleChoiceField(
  920. queryset=WirelessLAN.objects.all(),
  921. required=False,
  922. label=_('Wireless LANs'),
  923. query_params={
  924. 'group_id': '$wireless_lan_group',
  925. }
  926. )
  927. vlan_group = DynamicModelChoiceField(
  928. queryset=VLANGroup.objects.all(),
  929. required=False,
  930. label=_('VLAN group')
  931. )
  932. untagged_vlan = DynamicModelChoiceField(
  933. queryset=VLAN.objects.all(),
  934. required=False,
  935. label=_('Untagged VLAN'),
  936. query_params={
  937. 'group_id': '$vlan_group',
  938. 'available_on_device': '$device',
  939. }
  940. )
  941. tagged_vlans = DynamicModelMultipleChoiceField(
  942. queryset=VLAN.objects.all(),
  943. required=False,
  944. label=_('Tagged VLANs'),
  945. query_params={
  946. 'group_id': '$vlan_group',
  947. 'available_on_device': '$device',
  948. }
  949. )
  950. vrf = DynamicModelChoiceField(
  951. queryset=VRF.objects.all(),
  952. required=False,
  953. label=_('VRF')
  954. )
  955. wwn = forms.CharField(
  956. empty_value=None,
  957. required=False,
  958. label=_('WWN')
  959. )
  960. fieldsets = (
  961. ('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
  962. ('Addressing', ('vrf', 'mac_address', 'wwn')),
  963. ('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
  964. ('Related Interfaces', ('parent', 'bridge', 'lag')),
  965. ('PoE', ('poe_mode', 'poe_type')),
  966. ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
  967. ('Wireless', (
  968. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
  969. )),
  970. )
  971. class Meta:
  972. model = Interface
  973. fields = [
  974. 'device', 'module', 'vdcs', 'name', 'label', 'type', 'speed', 'duplex', 'enabled', 'parent', 'bridge', 'lag',
  975. 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode',
  976. 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans',
  977. 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
  978. ]
  979. widgets = {
  980. 'speed': NumberWithOptions(
  981. options=InterfaceSpeedChoices
  982. ),
  983. 'mode': HTMXSelect(),
  984. }
  985. labels = {
  986. 'mode': '802.1Q Mode',
  987. }
  988. class FrontPortForm(ModularDeviceComponentForm):
  989. rear_port = DynamicModelChoiceField(
  990. queryset=RearPort.objects.all(),
  991. query_params={
  992. 'device_id': '$device',
  993. }
  994. )
  995. fieldsets = (
  996. (None, (
  997. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  998. 'description', 'tags',
  999. )),
  1000. )
  1001. class Meta:
  1002. model = FrontPort
  1003. fields = [
  1004. 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
  1005. 'description', 'tags',
  1006. ]
  1007. class RearPortForm(ModularDeviceComponentForm):
  1008. fieldsets = (
  1009. (None, (
  1010. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1011. )),
  1012. )
  1013. class Meta:
  1014. model = RearPort
  1015. fields = [
  1016. 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
  1017. ]
  1018. class ModuleBayForm(DeviceComponentForm):
  1019. fieldsets = (
  1020. (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
  1021. )
  1022. class Meta:
  1023. model = ModuleBay
  1024. fields = [
  1025. 'device', 'name', 'label', 'position', 'description', 'tags',
  1026. ]
  1027. class DeviceBayForm(DeviceComponentForm):
  1028. fieldsets = (
  1029. (None, ('device', 'name', 'label', 'description', 'tags',)),
  1030. )
  1031. class Meta:
  1032. model = DeviceBay
  1033. fields = [
  1034. 'device', 'name', 'label', 'description', 'tags',
  1035. ]
  1036. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  1037. installed_device = forms.ModelChoiceField(
  1038. queryset=Device.objects.all(),
  1039. label=_('Child Device'),
  1040. help_text=_("Child devices must first be created and assigned to the site and rack of the parent device.")
  1041. )
  1042. def __init__(self, device_bay, *args, **kwargs):
  1043. super().__init__(*args, **kwargs)
  1044. self.fields['installed_device'].queryset = Device.objects.filter(
  1045. site=device_bay.device.site,
  1046. rack=device_bay.device.rack,
  1047. parent_bay__isnull=True,
  1048. device_type__u_height=0,
  1049. device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
  1050. ).exclude(pk=device_bay.device.pk)
  1051. class InventoryItemForm(DeviceComponentForm):
  1052. parent = DynamicModelChoiceField(
  1053. queryset=InventoryItem.objects.all(),
  1054. required=False,
  1055. query_params={
  1056. 'device_id': '$device'
  1057. }
  1058. )
  1059. role = DynamicModelChoiceField(
  1060. queryset=InventoryItemRole.objects.all(),
  1061. required=False
  1062. )
  1063. manufacturer = DynamicModelChoiceField(
  1064. queryset=Manufacturer.objects.all(),
  1065. required=False
  1066. )
  1067. # Assigned component selectors
  1068. consoleport = DynamicModelChoiceField(
  1069. queryset=ConsolePort.objects.all(),
  1070. required=False,
  1071. query_params={
  1072. 'device_id': '$device'
  1073. },
  1074. label=_('Console port')
  1075. )
  1076. consoleserverport = DynamicModelChoiceField(
  1077. queryset=ConsoleServerPort.objects.all(),
  1078. required=False,
  1079. query_params={
  1080. 'device_id': '$device'
  1081. },
  1082. label=_('Console server port')
  1083. )
  1084. frontport = DynamicModelChoiceField(
  1085. queryset=FrontPort.objects.all(),
  1086. required=False,
  1087. query_params={
  1088. 'device_id': '$device'
  1089. },
  1090. label=_('Front port')
  1091. )
  1092. interface = DynamicModelChoiceField(
  1093. queryset=Interface.objects.all(),
  1094. required=False,
  1095. query_params={
  1096. 'device_id': '$device'
  1097. },
  1098. label=_('Interface')
  1099. )
  1100. poweroutlet = DynamicModelChoiceField(
  1101. queryset=PowerOutlet.objects.all(),
  1102. required=False,
  1103. query_params={
  1104. 'device_id': '$device'
  1105. },
  1106. label=_('Power outlet')
  1107. )
  1108. powerport = DynamicModelChoiceField(
  1109. queryset=PowerPort.objects.all(),
  1110. required=False,
  1111. query_params={
  1112. 'device_id': '$device'
  1113. },
  1114. label=_('Power port')
  1115. )
  1116. rearport = DynamicModelChoiceField(
  1117. queryset=RearPort.objects.all(),
  1118. required=False,
  1119. query_params={
  1120. 'device_id': '$device'
  1121. },
  1122. label=_('Rear port')
  1123. )
  1124. fieldsets = (
  1125. ('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
  1126. ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
  1127. )
  1128. class Meta:
  1129. model = InventoryItem
  1130. fields = [
  1131. 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
  1132. 'description', 'tags',
  1133. ]
  1134. def __init__(self, *args, **kwargs):
  1135. instance = kwargs.get('instance')
  1136. initial = kwargs.get('initial', {}).copy()
  1137. component_type = initial.get('component_type')
  1138. component_id = initial.get('component_id')
  1139. # Used for picking the default active tab for component selection
  1140. self.no_component = True
  1141. if instance:
  1142. # When editing set the initial value for component selectin
  1143. for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
  1144. if type(instance.component) is component_model.model_class():
  1145. initial[component_model.model] = instance.component
  1146. self.no_component = False
  1147. break
  1148. elif component_type and component_id:
  1149. # When adding the InventoryItem from a component page
  1150. if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
  1151. if component := content_type.model_class().objects.filter(pk=component_id).first():
  1152. initial[content_type.model] = component
  1153. self.no_component = False
  1154. kwargs['initial'] = initial
  1155. super().__init__(*args, **kwargs)
  1156. # Specifically allow editing the device of IntentoryItems
  1157. if self.instance.pk:
  1158. self.fields['device'].disabled = False
  1159. def clean(self):
  1160. super().clean()
  1161. # Handle object assignment
  1162. selected_objects = [
  1163. field for field in (
  1164. 'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
  1165. ) if self.cleaned_data[field]
  1166. ]
  1167. if len(selected_objects) > 1:
  1168. raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
  1169. elif selected_objects:
  1170. self.instance.component = self.cleaned_data[selected_objects[0]]
  1171. else:
  1172. self.instance.component = None
  1173. # Device component roles
  1174. #
  1175. class InventoryItemRoleForm(NetBoxModelForm):
  1176. slug = SlugField()
  1177. fieldsets = (
  1178. ('Inventory Item Role', (
  1179. 'name', 'slug', 'color', 'description', 'tags',
  1180. )),
  1181. )
  1182. class Meta:
  1183. model = InventoryItemRole
  1184. fields = [
  1185. 'name', 'slug', 'color', 'description', 'tags',
  1186. ]
  1187. class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
  1188. device = DynamicModelChoiceField(
  1189. queryset=Device.objects.all(),
  1190. selector=True
  1191. )
  1192. primary_ip4 = DynamicModelChoiceField(
  1193. queryset=IPAddress.objects.all(),
  1194. label='Primary IPv4',
  1195. required=False,
  1196. query_params={
  1197. 'device_id': '$device',
  1198. 'family': '4',
  1199. }
  1200. )
  1201. primary_ip6 = DynamicModelChoiceField(
  1202. queryset=IPAddress.objects.all(),
  1203. label='Primary IPv6',
  1204. required=False,
  1205. query_params={
  1206. 'device_id': '$device',
  1207. 'family': '6',
  1208. }
  1209. )
  1210. fieldsets = (
  1211. ('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
  1212. ('Tenancy', ('tenant_group', 'tenant'))
  1213. )
  1214. class Meta:
  1215. model = VirtualDeviceContext
  1216. fields = [
  1217. 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
  1218. 'comments', 'tags'
  1219. ]