models.py 41 KB

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