forms.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402
  1. import re
  2. from django import forms
  3. from django.contrib.postgres.forms.array import SimpleArrayField
  4. from django.core.exceptions import ValidationError
  5. from django.db.models import Count, Q
  6. from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
  7. from ipam.models import IPAddress
  8. from tenancy.models import Tenant
  9. from utilities.forms import (
  10. APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkImportForm, CommentField,
  11. CSVDataField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled,
  12. SmallTextarea, SlugField,
  13. )
  14. from .formfields import MACAddressFormField
  15. from .models import (
  16. DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
  17. ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
  18. Interface, IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
  19. Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
  20. RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD
  21. )
  22. FORM_STATUS_CHOICES = [
  23. ['', '---------'],
  24. ]
  25. FORM_STATUS_CHOICES += STATUS_CHOICES
  26. DEVICE_BY_PK_RE = '{\d+\}'
  27. def get_device_by_name_or_pk(name):
  28. """
  29. Attempt to retrieve a device by either its name or primary key ('{pk}').
  30. """
  31. if re.match(DEVICE_BY_PK_RE, name):
  32. pk = name.strip('{}')
  33. device = Device.objects.get(pk=pk)
  34. else:
  35. device = Device.objects.get(name=name)
  36. return device
  37. def validate_connection_status(value):
  38. """
  39. Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
  40. """
  41. if value.lower() not in ['planned', 'connected']:
  42. raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
  43. #
  44. # Sites
  45. #
  46. class SiteForm(BootstrapMixin, CustomFieldForm):
  47. slug = SlugField()
  48. comments = CommentField()
  49. class Meta:
  50. model = Site
  51. fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name',
  52. 'contact_phone', 'contact_email', 'comments']
  53. widgets = {
  54. 'physical_address': SmallTextarea(attrs={'rows': 3}),
  55. 'shipping_address': SmallTextarea(attrs={'rows': 3}),
  56. }
  57. help_texts = {
  58. 'name': "Full name of the site",
  59. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  60. 'asn': "BGP autonomous system number",
  61. 'physical_address': "Physical location of the building (e.g. for GPS)",
  62. 'shipping_address': "If different from the physical address"
  63. }
  64. class SiteFromCSVForm(forms.ModelForm):
  65. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  66. error_messages={'invalid_choice': 'Tenant not found.'})
  67. class Meta:
  68. model = Site
  69. fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
  70. class SiteImportForm(BootstrapMixin, BulkImportForm):
  71. csv = CSVDataField(csv_form=SiteFromCSVForm)
  72. class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  73. pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
  74. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  75. asn = forms.IntegerField(min_value=1, max_value=4294967295, required=False, label='ASN')
  76. class Meta:
  77. nullable_fields = ['tenant', 'asn']
  78. class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
  79. model = Site
  80. q = forms.CharField(required=False, label='Search')
  81. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug',
  82. null_option=(0, 'None'))
  83. #
  84. # Rack groups
  85. #
  86. class RackGroupForm(BootstrapMixin, forms.ModelForm):
  87. slug = SlugField()
  88. class Meta:
  89. model = RackGroup
  90. fields = ['site', 'name', 'slug']
  91. class RackGroupFilterForm(BootstrapMixin, forms.Form):
  92. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('rack_groups')), to_field_name='slug')
  93. #
  94. # Rack roles
  95. #
  96. class RackRoleForm(BootstrapMixin, forms.ModelForm):
  97. slug = SlugField()
  98. class Meta:
  99. model = RackRole
  100. fields = ['name', 'slug', 'color']
  101. #
  102. # Racks
  103. #
  104. class RackForm(BootstrapMixin, CustomFieldForm):
  105. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
  106. api_url='/api/dcim/rack-groups/?site_id={{site}}',
  107. ))
  108. comments = CommentField()
  109. class Meta:
  110. model = Rack
  111. fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
  112. 'comments']
  113. help_texts = {
  114. 'site': "The site at which the rack exists",
  115. 'name': "Organizational rack name",
  116. 'facility_id': "The unique rack ID assigned by the facility",
  117. 'u_height': "Height in rack units",
  118. }
  119. widgets = {
  120. 'site': forms.Select(attrs={'filter-for': 'group'}),
  121. }
  122. def __init__(self, *args, **kwargs):
  123. super(RackForm, self).__init__(*args, **kwargs)
  124. # Limit rack group choices
  125. if self.is_bound and self.data.get('site'):
  126. self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
  127. elif self.initial.get('site'):
  128. self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
  129. else:
  130. self.fields['group'].choices = []
  131. class RackFromCSVForm(forms.ModelForm):
  132. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
  133. error_messages={'invalid_choice': 'Site not found.'})
  134. group_name = forms.CharField(required=False)
  135. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  136. error_messages={'invalid_choice': 'Tenant not found.'})
  137. role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
  138. error_messages={'invalid_choice': 'Role not found.'})
  139. type = forms.CharField(required=False)
  140. class Meta:
  141. model = Rack
  142. fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
  143. 'desc_units']
  144. def clean(self):
  145. site = self.cleaned_data.get('site')
  146. group = self.cleaned_data.get('group_name')
  147. # Validate rack group
  148. if site and group:
  149. try:
  150. self.instance.group = RackGroup.objects.get(site=site, name=group)
  151. except RackGroup.DoesNotExist:
  152. self.add_error('group_name', "Invalid rack group ({})".format(group))
  153. def clean_type(self):
  154. rack_type = self.cleaned_data['type']
  155. if not rack_type:
  156. return None
  157. try:
  158. choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
  159. return choices[rack_type.lower()]
  160. except KeyError:
  161. raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
  162. rack_type,
  163. ', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
  164. ))
  165. class RackImportForm(BootstrapMixin, BulkImportForm):
  166. csv = CSVDataField(csv_form=RackFromCSVForm)
  167. class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  168. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  169. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site')
  170. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group')
  171. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  172. role = forms.ModelChoiceField(queryset=RackRole.objects.all(), required=False)
  173. type = forms.ChoiceField(choices=add_blank_choice(RACK_TYPE_CHOICES), required=False, label='Type')
  174. width = forms.ChoiceField(choices=add_blank_choice(RACK_WIDTH_CHOICES), required=False, label='Width')
  175. u_height = forms.IntegerField(required=False, label='Height (U)')
  176. comments = CommentField(widget=SmallTextarea)
  177. class Meta:
  178. nullable_fields = ['group', 'tenant', 'role', 'comments']
  179. class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
  180. model = Rack
  181. q = forms.CharField(required=False, label='Search')
  182. site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
  183. group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
  184. .annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
  185. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
  186. null_option=(0, 'None'))
  187. role = FilterChoiceField(queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
  188. null_option=(0, 'None'))
  189. #
  190. # Rack reservations
  191. #
  192. class RackReservationForm(BootstrapMixin, forms.ModelForm):
  193. units = SimpleArrayField(forms.IntegerField(), widget=ArrayFieldSelectMultiple(attrs={'size': 10}))
  194. class Meta:
  195. model = RackReservation
  196. fields = ['units', 'description']
  197. def __init__(self, *args, **kwargs):
  198. super(RackReservationForm, self).__init__(*args, **kwargs)
  199. # Populate rack unit choices
  200. self.fields['units'].widget.choices = self._get_unit_choices()
  201. def _get_unit_choices(self):
  202. rack = self.instance.rack
  203. reserved_units = []
  204. for resv in rack.reservations.exclude(pk=self.instance.pk):
  205. for u in resv.units:
  206. reserved_units.append(u)
  207. unit_choices = [(u, {'label': str(u), 'disabled': u in reserved_units}) for u in rack.units]
  208. return unit_choices
  209. #
  210. # Manufacturers
  211. #
  212. class ManufacturerForm(BootstrapMixin, forms.ModelForm):
  213. slug = SlugField()
  214. class Meta:
  215. model = Manufacturer
  216. fields = ['name', 'slug']
  217. #
  218. # Device types
  219. #
  220. class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
  221. slug = SlugField(slug_source='model')
  222. class Meta:
  223. model = DeviceType
  224. fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
  225. 'is_pdu', 'is_network_device', 'subdevice_role', 'interface_ordering', 'comments']
  226. labels = {
  227. 'interface_ordering': 'Order interfaces by',
  228. }
  229. class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  230. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  231. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
  232. u_height = forms.IntegerField(min_value=1, required=False)
  233. interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
  234. class Meta:
  235. nullable_fields = []
  236. class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
  237. model = DeviceType
  238. q = forms.CharField(required=False, label='Search')
  239. manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
  240. to_field_name='slug')
  241. #
  242. # Device component templates
  243. #
  244. class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
  245. class Meta:
  246. model = ConsolePortTemplate
  247. fields = ['device_type', 'name']
  248. widgets = {
  249. 'device_type': forms.HiddenInput(),
  250. }
  251. class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
  252. name_pattern = ExpandableNameField(label='Name')
  253. class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  254. class Meta:
  255. model = ConsoleServerPortTemplate
  256. fields = ['device_type', 'name']
  257. widgets = {
  258. 'device_type': forms.HiddenInput(),
  259. }
  260. class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
  261. name_pattern = ExpandableNameField(label='Name')
  262. class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
  263. class Meta:
  264. model = PowerPortTemplate
  265. fields = ['device_type', 'name']
  266. widgets = {
  267. 'device_type': forms.HiddenInput(),
  268. }
  269. class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
  270. name_pattern = ExpandableNameField(label='Name')
  271. class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
  272. class Meta:
  273. model = PowerOutletTemplate
  274. fields = ['device_type', 'name']
  275. widgets = {
  276. 'device_type': forms.HiddenInput(),
  277. }
  278. class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
  279. name_pattern = ExpandableNameField(label='Name')
  280. class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
  281. class Meta:
  282. model = InterfaceTemplate
  283. fields = ['device_type', 'name', 'form_factor', 'mgmt_only']
  284. widgets = {
  285. 'device_type': forms.HiddenInput(),
  286. }
  287. class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
  288. name_pattern = ExpandableNameField(label='Name')
  289. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  290. mgmt_only = forms.BooleanField(required=False, label='OOB Management')
  291. class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
  292. pk = forms.ModelMultipleChoiceField(queryset=InterfaceTemplate.objects.all(), widget=forms.MultipleHiddenInput)
  293. form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
  294. class Meta:
  295. nullable_fields = []
  296. class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
  297. class Meta:
  298. model = DeviceBayTemplate
  299. fields = ['device_type', 'name']
  300. widgets = {
  301. 'device_type': forms.HiddenInput(),
  302. }
  303. class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
  304. name_pattern = ExpandableNameField(label='Name')
  305. #
  306. # Device roles
  307. #
  308. class DeviceRoleForm(BootstrapMixin, forms.ModelForm):
  309. slug = SlugField()
  310. class Meta:
  311. model = DeviceRole
  312. fields = ['name', 'slug', 'color']
  313. #
  314. # Platforms
  315. #
  316. class PlatformForm(BootstrapMixin, forms.ModelForm):
  317. slug = SlugField()
  318. class Meta:
  319. model = Platform
  320. fields = ['name', 'slug']
  321. #
  322. # Devices
  323. #
  324. class DeviceForm(BootstrapMixin, CustomFieldForm):
  325. site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
  326. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
  327. api_url='/api/dcim/racks/?site_id={{site}}',
  328. display_field='display_name',
  329. attrs={'filter-for': 'position'}
  330. ))
  331. position = forms.TypedChoiceField(required=False, empty_value=None,
  332. help_text="The lowest-numbered unit occupied by the device",
  333. widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
  334. disabled_indicator='device'))
  335. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
  336. widget=forms.Select(attrs={'filter-for': 'device_type'}))
  337. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Device type', widget=APISelect(
  338. api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
  339. display_field='model'
  340. ))
  341. comments = CommentField()
  342. class Meta:
  343. model = Device
  344. fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position',
  345. 'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'comments']
  346. help_texts = {
  347. 'device_role': "The function this device serves",
  348. 'serial': "Chassis serial number",
  349. }
  350. widgets = {
  351. 'face': forms.Select(attrs={'filter-for': 'position'}),
  352. 'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
  353. }
  354. def __init__(self, *args, **kwargs):
  355. super(DeviceForm, self).__init__(*args, **kwargs)
  356. if self.instance.pk:
  357. # Initialize helper selections
  358. self.initial['site'] = self.instance.rack.site
  359. self.initial['manufacturer'] = self.instance.device_type.manufacturer
  360. # Compile list of choices for primary IPv4 and IPv6 addresses
  361. for family in [4, 6]:
  362. ip_choices = []
  363. interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
  364. ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  365. nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
  366. .select_related('nat_inside__interface')
  367. ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
  368. self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
  369. # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
  370. # can be flipped from one face to another.
  371. self.fields['position'].widget.attrs['api-url'] += '&exclude={}'.format(self.instance.pk)
  372. else:
  373. # An object that doesn't exist yet can't have any IPs assigned to it
  374. self.fields['primary_ip4'].choices = []
  375. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  376. self.fields['primary_ip6'].choices = []
  377. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  378. # Limit rack choices
  379. if self.is_bound and self.data.get('site'):
  380. self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
  381. elif self.initial.get('site'):
  382. self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
  383. else:
  384. self.fields['rack'].choices = []
  385. # Rack position
  386. pk = self.instance.pk if self.instance.pk else None
  387. try:
  388. if self.is_bound and self.data.get('rack') and str(self.data.get('face')):
  389. position_choices = Rack.objects.get(pk=self.data['rack'])\
  390. .get_rack_units(face=self.data.get('face'), exclude=pk)
  391. elif self.initial.get('rack') and str(self.initial.get('face')):
  392. position_choices = Rack.objects.get(pk=self.initial['rack'])\
  393. .get_rack_units(face=self.initial.get('face'), exclude=pk)
  394. else:
  395. position_choices = []
  396. except Rack.DoesNotExist:
  397. position_choices = []
  398. self.fields['position'].choices = [('', '---------')] + [
  399. (p['id'], {
  400. 'label': p['name'],
  401. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  402. }) for p in position_choices
  403. ]
  404. # Limit device_type choices
  405. if self.is_bound:
  406. self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
  407. .select_related('manufacturer')
  408. elif self.initial.get('manufacturer'):
  409. self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
  410. .select_related('manufacturer')
  411. else:
  412. self.fields['device_type'].choices = []
  413. # Disable rack assignment if this is a child device installed in a parent device
  414. if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'):
  415. self.fields['site'].disabled = True
  416. self.fields['rack'].disabled = True
  417. self.initial['site'] = self.instance.parent_bay.device.rack.site_id
  418. self.initial['rack'] = self.instance.parent_bay.device.rack_id
  419. class BaseDeviceFromCSVForm(forms.ModelForm):
  420. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
  421. error_messages={'invalid_choice': 'Invalid device role.'})
  422. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  423. error_messages={'invalid_choice': 'Tenant not found.'})
  424. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
  425. error_messages={'invalid_choice': 'Invalid manufacturer.'})
  426. model_name = forms.CharField()
  427. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
  428. error_messages={'invalid_choice': 'Invalid platform.'})
  429. class Meta:
  430. fields = []
  431. model = Device
  432. def clean(self):
  433. manufacturer = self.cleaned_data.get('manufacturer')
  434. model_name = self.cleaned_data.get('model_name')
  435. # Validate device type
  436. if manufacturer and model_name:
  437. try:
  438. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  439. except DeviceType.DoesNotExist:
  440. self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
  441. class DeviceFromCSVForm(BaseDeviceFromCSVForm):
  442. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
  443. 'invalid_choice': 'Invalid site name.',
  444. })
  445. rack_name = forms.CharField()
  446. face = forms.CharField(required=False)
  447. class Meta(BaseDeviceFromCSVForm.Meta):
  448. fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
  449. 'site', 'rack_name', 'position', 'face']
  450. def clean(self):
  451. super(DeviceFromCSVForm, self).clean()
  452. site = self.cleaned_data.get('site')
  453. rack_name = self.cleaned_data.get('rack_name')
  454. # Validate rack
  455. if site and rack_name:
  456. try:
  457. self.instance.rack = Rack.objects.get(site=site, name=rack_name)
  458. except Rack.DoesNotExist:
  459. self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
  460. def clean_face(self):
  461. face = self.cleaned_data['face']
  462. if not face:
  463. return None
  464. try:
  465. return {
  466. 'front': 0,
  467. 'rear': 1,
  468. }[face.lower()]
  469. except KeyError:
  470. raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
  471. class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
  472. parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
  473. error_messages={'invalid_choice': 'Parent device not found.'})
  474. device_bay_name = forms.CharField(required=False)
  475. class Meta(BaseDeviceFromCSVForm.Meta):
  476. fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
  477. 'parent', 'device_bay_name']
  478. def clean(self):
  479. super(ChildDeviceFromCSVForm, self).clean()
  480. parent = self.cleaned_data.get('parent')
  481. device_bay_name = self.cleaned_data.get('device_bay_name')
  482. # Validate device bay
  483. if parent and device_bay_name:
  484. try:
  485. device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
  486. if device_bay.installed_device:
  487. self.add_error('device_bay_name',
  488. "Device bay ({} {}) is already occupied".format(parent, device_bay_name))
  489. else:
  490. self.instance.parent_bay = device_bay
  491. except DeviceBay.DoesNotExist:
  492. self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
  493. class DeviceImportForm(BootstrapMixin, BulkImportForm):
  494. csv = CSVDataField(csv_form=DeviceFromCSVForm)
  495. class ChildDeviceImportForm(BootstrapMixin, BulkImportForm):
  496. csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
  497. class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  498. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  499. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
  500. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
  501. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  502. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False)
  503. status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
  504. serial = forms.CharField(max_length=50, required=False, label='Serial Number')
  505. class Meta:
  506. nullable_fields = ['tenant', 'platform']
  507. class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
  508. model = Device
  509. q = forms.CharField(required=False, label='Search')
  510. site = FilterChoiceField(
  511. queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
  512. to_field_name='slug',
  513. )
  514. rack_group_id = FilterChoiceField(
  515. queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
  516. label='Rack Group',
  517. )
  518. role = FilterChoiceField(
  519. queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
  520. to_field_name='slug',
  521. )
  522. tenant = FilterChoiceField(
  523. queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug',
  524. null_option=(0, 'None'),
  525. )
  526. manufacturer_id = FilterChoiceField(
  527. queryset=Manufacturer.objects.all(),
  528. label='Manufacturer',
  529. )
  530. device_type_id = FilterChoiceField(
  531. queryset=DeviceType.objects.select_related('manufacturer').order_by('model').annotate(
  532. filter_count=Count('instances'),
  533. ),
  534. label='Model',
  535. )
  536. platform = FilterChoiceField(
  537. queryset=Platform.objects.annotate(filter_count=Count('devices')),
  538. to_field_name='slug',
  539. null_option=(0, 'None'),
  540. )
  541. status = forms.NullBooleanField(
  542. required=False,
  543. widget=forms.Select(choices=FORM_STATUS_CHOICES),
  544. )
  545. mac_address = forms.CharField(
  546. required=False,
  547. label='MAC address',
  548. )
  549. #
  550. # Bulk device component creation
  551. #
  552. class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form):
  553. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  554. name_pattern = ExpandableNameField(label='Name')
  555. class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
  556. class Meta:
  557. model = Interface
  558. fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description']
  559. #
  560. # Console ports
  561. #
  562. class ConsolePortForm(BootstrapMixin, forms.ModelForm):
  563. class Meta:
  564. model = ConsolePort
  565. fields = ['device', 'name']
  566. widgets = {
  567. 'device': forms.HiddenInput(),
  568. }
  569. class ConsolePortCreateForm(BootstrapMixin, forms.Form):
  570. name_pattern = ExpandableNameField(label='Name')
  571. class ConsoleConnectionCSVForm(forms.Form):
  572. console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
  573. to_field_name='name',
  574. error_messages={'invalid_choice': 'Console server not found'})
  575. cs_port = forms.CharField()
  576. device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  577. error_messages={'invalid_choice': 'Device not found'})
  578. console_port = forms.CharField()
  579. status = forms.CharField(validators=[validate_connection_status])
  580. def clean(self):
  581. # Validate console server port
  582. if self.cleaned_data.get('console_server'):
  583. try:
  584. cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
  585. name=self.cleaned_data['cs_port'])
  586. if ConsolePort.objects.filter(cs_port=cs_port):
  587. raise forms.ValidationError("Console server port is already occupied (by {} {})"
  588. .format(cs_port.connected_console.device, cs_port.connected_console))
  589. except ConsoleServerPort.DoesNotExist:
  590. raise forms.ValidationError("Invalid console server port ({} {})"
  591. .format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
  592. # Validate console port
  593. if self.cleaned_data.get('device'):
  594. try:
  595. console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
  596. name=self.cleaned_data['console_port'])
  597. if console_port.cs_port:
  598. raise forms.ValidationError("Console port is already connected (to {} {})"
  599. .format(console_port.cs_port.device, console_port.cs_port))
  600. except ConsolePort.DoesNotExist:
  601. raise forms.ValidationError("Invalid console port ({} {})"
  602. .format(self.cleaned_data['device'], self.cleaned_data['console_port']))
  603. class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
  604. csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
  605. def clean(self):
  606. records = self.cleaned_data.get('csv')
  607. if not records:
  608. return
  609. connection_list = []
  610. for i, record in enumerate(records, start=1):
  611. form = self.fields['csv'].csv_form(data=record)
  612. if form.is_valid():
  613. console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
  614. name=form.cleaned_data['console_port'])
  615. console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
  616. name=form.cleaned_data['cs_port'])
  617. if form.cleaned_data['status'] == 'planned':
  618. console_port.connection_status = CONNECTION_STATUS_PLANNED
  619. else:
  620. console_port.connection_status = CONNECTION_STATUS_CONNECTED
  621. connection_list.append(console_port)
  622. else:
  623. for field, errors in form.errors.items():
  624. for e in errors:
  625. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  626. self.cleaned_data['csv'] = connection_list
  627. class ConsolePortConnectionForm(BootstrapMixin, forms.ModelForm):
  628. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  629. widget=forms.Select(attrs={'filter-for': 'console_server'}))
  630. console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
  631. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
  632. display_field='display_name',
  633. attrs={'filter-for': 'cs_port'}))
  634. livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
  635. query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
  636. )
  637. cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
  638. widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
  639. disabled_indicator='connected_console'))
  640. class Meta:
  641. model = ConsolePort
  642. fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
  643. labels = {
  644. 'cs_port': 'Port',
  645. 'connection_status': 'Status',
  646. }
  647. def __init__(self, *args, **kwargs):
  648. super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
  649. if not self.instance.pk:
  650. raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
  651. self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
  652. self.fields['cs_port'].required = True
  653. self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
  654. # Initialize console server choices
  655. if self.is_bound and self.data.get('rack'):
  656. self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
  657. elif self.initial.get('rack'):
  658. self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
  659. else:
  660. self.fields['console_server'].choices = []
  661. # Initialize CS port choices
  662. if self.is_bound:
  663. self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.data['console_server'])
  664. elif self.initial.get('console_server', None):
  665. self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.initial['console_server'])
  666. else:
  667. self.fields['cs_port'].choices = []
  668. #
  669. # Console server ports
  670. #
  671. class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
  672. class Meta:
  673. model = ConsoleServerPort
  674. fields = ['device', 'name']
  675. widgets = {
  676. 'device': forms.HiddenInput(),
  677. }
  678. class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
  679. name_pattern = ExpandableNameField(label='Name')
  680. class ConsoleServerPortConnectionForm(BootstrapMixin, forms.Form):
  681. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  682. widget=forms.Select(attrs={'filter-for': 'device'}))
  683. device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  684. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
  685. display_field='display_name', attrs={'filter-for': 'port'}))
  686. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  687. query_key='q', query_url='dcim-api:device_list', field_to_update='device')
  688. )
  689. port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
  690. widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
  691. disabled_indicator='cs_port'))
  692. connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
  693. widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
  694. class Meta:
  695. fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
  696. labels = {
  697. 'connection_status': 'Status',
  698. }
  699. def __init__(self, consoleserverport, *args, **kwargs):
  700. super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
  701. self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
  702. # Initialize device choices
  703. if self.is_bound and self.data.get('rack'):
  704. self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
  705. elif self.initial.get('rack', None):
  706. self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
  707. else:
  708. self.fields['device'].choices = []
  709. # Initialize port choices
  710. if self.is_bound:
  711. self.fields['port'].queryset = ConsolePort.objects.filter(device__pk=self.data['device'])
  712. elif self.initial.get('device', None):
  713. self.fields['port'].queryset = ConsolePort.objects.filter(device_pk=self.initial['device'])
  714. else:
  715. self.fields['port'].choices = []
  716. #
  717. # Power ports
  718. #
  719. class PowerPortForm(BootstrapMixin, forms.ModelForm):
  720. class Meta:
  721. model = PowerPort
  722. fields = ['device', 'name']
  723. widgets = {
  724. 'device': forms.HiddenInput(),
  725. }
  726. class PowerPortCreateForm(BootstrapMixin, forms.Form):
  727. name_pattern = ExpandableNameField(label='Name')
  728. class PowerConnectionCSVForm(forms.Form):
  729. pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
  730. error_messages={'invalid_choice': 'PDU not found.'})
  731. power_outlet = forms.CharField()
  732. device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  733. error_messages={'invalid_choice': 'Device not found'})
  734. power_port = forms.CharField()
  735. status = forms.CharField(validators=[validate_connection_status])
  736. def clean(self):
  737. # Validate power outlet
  738. if self.cleaned_data.get('pdu'):
  739. try:
  740. power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
  741. name=self.cleaned_data['power_outlet'])
  742. if PowerPort.objects.filter(power_outlet=power_outlet):
  743. raise forms.ValidationError("Power outlet is already occupied (by {} {})"
  744. .format(power_outlet.connected_port.device,
  745. power_outlet.connected_port))
  746. except PowerOutlet.DoesNotExist:
  747. raise forms.ValidationError("Invalid PDU port ({} {})"
  748. .format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
  749. # Validate power port
  750. if self.cleaned_data.get('device'):
  751. try:
  752. power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
  753. name=self.cleaned_data['power_port'])
  754. if power_port.power_outlet:
  755. raise forms.ValidationError("Power port is already connected (to {} {})"
  756. .format(power_port.power_outlet.device, power_port.power_outlet))
  757. except PowerPort.DoesNotExist:
  758. raise forms.ValidationError("Invalid power port ({} {})"
  759. .format(self.cleaned_data['device'], self.cleaned_data['power_port']))
  760. class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
  761. csv = CSVDataField(csv_form=PowerConnectionCSVForm)
  762. def clean(self):
  763. records = self.cleaned_data.get('csv')
  764. if not records:
  765. return
  766. connection_list = []
  767. for i, record in enumerate(records, start=1):
  768. form = self.fields['csv'].csv_form(data=record)
  769. if form.is_valid():
  770. power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
  771. name=form.cleaned_data['power_port'])
  772. power_port.power_outlet = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
  773. name=form.cleaned_data['power_outlet'])
  774. if form.cleaned_data['status'] == 'planned':
  775. power_port.connection_status = CONNECTION_STATUS_PLANNED
  776. else:
  777. power_port.connection_status = CONNECTION_STATUS_CONNECTED
  778. connection_list.append(power_port)
  779. else:
  780. for field, errors in form.errors.items():
  781. for e in errors:
  782. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  783. self.cleaned_data['csv'] = connection_list
  784. class PowerPortConnectionForm(BootstrapMixin, forms.ModelForm):
  785. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  786. widget=forms.Select(attrs={'filter-for': 'pdu'}))
  787. pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
  788. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
  789. display_field='display_name', attrs={'filter-for': 'power_outlet'}))
  790. livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
  791. query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
  792. )
  793. power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
  794. widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
  795. disabled_indicator='connected_port'))
  796. class Meta:
  797. model = PowerPort
  798. fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
  799. labels = {
  800. 'power_outlet': 'Outlet',
  801. 'connection_status': 'Status',
  802. }
  803. def __init__(self, *args, **kwargs):
  804. super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
  805. if not self.instance.pk:
  806. raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
  807. self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
  808. self.fields['power_outlet'].required = True
  809. self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
  810. # Initialize PDU choices
  811. if self.is_bound and self.data.get('rack'):
  812. self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
  813. elif self.initial.get('rack', None):
  814. self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
  815. else:
  816. self.fields['pdu'].choices = []
  817. # Initialize power outlet choices
  818. if self.is_bound:
  819. self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.data['pdu'])
  820. elif self.initial.get('pdu', None):
  821. self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.initial['pdu'])
  822. else:
  823. self.fields['power_outlet'].choices = []
  824. #
  825. # Power outlets
  826. #
  827. class PowerOutletForm(BootstrapMixin, forms.ModelForm):
  828. class Meta:
  829. model = PowerOutlet
  830. fields = ['device', 'name']
  831. widgets = {
  832. 'device': forms.HiddenInput(),
  833. }
  834. class PowerOutletCreateForm(BootstrapMixin, forms.Form):
  835. name_pattern = ExpandableNameField(label='Name')
  836. class PowerOutletConnectionForm(BootstrapMixin, forms.Form):
  837. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  838. widget=forms.Select(attrs={'filter-for': 'device'}))
  839. device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  840. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
  841. display_field='display_name', attrs={'filter-for': 'port'}))
  842. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  843. query_key='q', query_url='dcim-api:device_list', field_to_update='device')
  844. )
  845. port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
  846. widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
  847. disabled_indicator='power_outlet'))
  848. connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
  849. widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
  850. class Meta:
  851. fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
  852. labels = {
  853. 'connection_status': 'Status',
  854. }
  855. def __init__(self, poweroutlet, *args, **kwargs):
  856. super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
  857. self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
  858. # Initialize device choices
  859. if self.is_bound and self.data.get('rack'):
  860. self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
  861. elif self.initial.get('rack', None):
  862. self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
  863. else:
  864. self.fields['device'].choices = []
  865. # Initialize port choices
  866. if self.is_bound:
  867. self.fields['port'].queryset = PowerPort.objects.filter(device__pk=self.data['device'])
  868. elif self.initial.get('device', None):
  869. self.fields['port'].queryset = PowerPort.objects.filter(device_pk=self.initial['device'])
  870. else:
  871. self.fields['port'].choices = []
  872. #
  873. # Interfaces
  874. #
  875. class InterfaceForm(BootstrapMixin, forms.ModelForm):
  876. class Meta:
  877. model = Interface
  878. fields = ['device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description']
  879. widgets = {
  880. 'device': forms.HiddenInput(),
  881. }
  882. class InterfaceCreateForm(BootstrapMixin, forms.Form):
  883. name_pattern = ExpandableNameField(label='Name')
  884. form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
  885. mac_address = MACAddressFormField(required=False, label='MAC Address')
  886. mgmt_only = forms.BooleanField(required=False, label='OOB Management')
  887. description = forms.CharField(max_length=100, required=False)
  888. class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
  889. pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
  890. form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
  891. description = forms.CharField(max_length=100, required=False)
  892. class Meta:
  893. nullable_fields = ['description']
  894. #
  895. # Interface connections
  896. #
  897. class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
  898. interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
  899. site_b = forms.ModelChoiceField(queryset=Site.objects.all(), label='Site', required=False,
  900. widget=forms.Select(attrs={'filter-for': 'rack_b'}))
  901. rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  902. widget=APISelect(api_url='/api/dcim/racks/?site_id={{site_b}}',
  903. attrs={'filter-for': 'device_b'}))
  904. device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  905. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
  906. display_field='display_name',
  907. attrs={'filter-for': 'interface_b'}))
  908. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  909. query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
  910. )
  911. interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
  912. widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
  913. disabled_indicator='is_connected'))
  914. class Meta:
  915. model = InterfaceConnection
  916. fields = ['interface_a', 'site_b', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
  917. def __init__(self, device_a, *args, **kwargs):
  918. super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
  919. # Initialize interface A choices
  920. device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL)\
  921. .select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
  922. self.fields['interface_a'].choices = [
  923. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
  924. ]
  925. # Initialize rack_b choices if site_b is set
  926. if self.is_bound and self.data.get('site_b'):
  927. self.fields['rack_b'].queryset = Rack.objects.filter(site__pk=self.data['site_b'])
  928. elif self.initial.get('site_b'):
  929. self.fields['rack_b'].queryset = Rack.objects.filter(site=self.initial['site_b'])
  930. else:
  931. self.fields['rack_b'].choices = []
  932. # Initialize device_b choices if rack_b is set
  933. if self.is_bound and self.data.get('rack_b'):
  934. self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
  935. elif self.initial.get('rack_b'):
  936. self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
  937. else:
  938. self.fields['device_b'].choices = []
  939. # Initialize interface_b choices if device_b is set
  940. if self.is_bound:
  941. device_b_interfaces = Interface.objects.filter(device=self.data['device_b'])\
  942. .exclude(form_factor=IFACE_FF_VIRTUAL)\
  943. .select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
  944. elif self.initial.get('device_b'):
  945. device_b_interfaces = Interface.objects.filter(device=self.initial['device_b'])\
  946. .exclude(form_factor=IFACE_FF_VIRTUAL)\
  947. .select_related('circuit_termination', 'connected_as_a', 'connected_as_b')
  948. else:
  949. device_b_interfaces = []
  950. self.fields['interface_b'].choices = [
  951. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
  952. ]
  953. class InterfaceConnectionCSVForm(forms.Form):
  954. device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  955. error_messages={'invalid_choice': 'Device A not found.'})
  956. interface_a = forms.CharField()
  957. device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  958. error_messages={'invalid_choice': 'Device B not found.'})
  959. interface_b = forms.CharField()
  960. status = forms.CharField(validators=[validate_connection_status])
  961. def clean(self):
  962. # Validate interface A
  963. if self.cleaned_data.get('device_a'):
  964. try:
  965. interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
  966. name=self.cleaned_data['interface_a'])
  967. except Interface.DoesNotExist:
  968. raise forms.ValidationError("Invalid interface ({} {})"
  969. .format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
  970. try:
  971. InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
  972. raise forms.ValidationError("{} {} is already connected"
  973. .format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
  974. except InterfaceConnection.DoesNotExist:
  975. pass
  976. # Validate interface B
  977. if self.cleaned_data.get('device_b'):
  978. try:
  979. interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
  980. name=self.cleaned_data['interface_b'])
  981. except Interface.DoesNotExist:
  982. raise forms.ValidationError("Invalid interface ({} {})"
  983. .format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
  984. try:
  985. InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
  986. raise forms.ValidationError("{} {} is already connected"
  987. .format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
  988. except InterfaceConnection.DoesNotExist:
  989. pass
  990. class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
  991. csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
  992. def clean(self):
  993. records = self.cleaned_data.get('csv')
  994. if not records:
  995. return
  996. connection_list = []
  997. occupied_interfaces = []
  998. for i, record in enumerate(records, start=1):
  999. form = self.fields['csv'].csv_form(data=record)
  1000. if form.is_valid():
  1001. interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
  1002. name=form.cleaned_data['interface_a'])
  1003. if interface_a in occupied_interfaces:
  1004. raise forms.ValidationError("{} {} found in multiple connections"
  1005. .format(interface_a.device.name, interface_a.name))
  1006. interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
  1007. name=form.cleaned_data['interface_b'])
  1008. if interface_b in occupied_interfaces:
  1009. raise forms.ValidationError("{} {} found in multiple connections"
  1010. .format(interface_b.device.name, interface_b.name))
  1011. connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
  1012. if form.cleaned_data['status'] == 'planned':
  1013. connection.connection_status = CONNECTION_STATUS_PLANNED
  1014. else:
  1015. connection.connection_status = CONNECTION_STATUS_CONNECTED
  1016. connection_list.append(connection)
  1017. occupied_interfaces.append(interface_a)
  1018. occupied_interfaces.append(interface_b)
  1019. else:
  1020. for field, errors in form.errors.items():
  1021. for e in errors:
  1022. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  1023. self.cleaned_data['csv'] = connection_list
  1024. class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
  1025. confirm = forms.BooleanField(required=True)
  1026. # Used for HTTP redirect upon successful deletion
  1027. device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
  1028. #
  1029. # Device bays
  1030. #
  1031. class DeviceBayForm(BootstrapMixin, forms.ModelForm):
  1032. class Meta:
  1033. model = DeviceBay
  1034. fields = ['device', 'name']
  1035. widgets = {
  1036. 'device': forms.HiddenInput(),
  1037. }
  1038. class DeviceBayCreateForm(BootstrapMixin, forms.Form):
  1039. name_pattern = ExpandableNameField(label='Name')
  1040. class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
  1041. installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
  1042. help_text="Child devices must first be created within the rack occupied "
  1043. "by the parent device. Then they can be assigned to a bay.")
  1044. def __init__(self, device_bay, *args, **kwargs):
  1045. super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
  1046. children_queryset = Device.objects.filter(rack=device_bay.device.rack,
  1047. parent_bay__isnull=True,
  1048. device_type__u_height=0,
  1049. device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\
  1050. .exclude(pk=device_bay.device.pk)
  1051. self.fields['installed_device'].queryset = children_queryset
  1052. #
  1053. # Connections
  1054. #
  1055. class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
  1056. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1057. class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
  1058. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1059. class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
  1060. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  1061. #
  1062. # IP addresses
  1063. #
  1064. class IPAddressForm(BootstrapMixin, CustomFieldForm):
  1065. set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
  1066. class Meta:
  1067. model = IPAddress
  1068. fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'description']
  1069. def __init__(self, device, *args, **kwargs):
  1070. super(IPAddressForm, self).__init__(*args, **kwargs)
  1071. self.fields['vrf'].empty_label = 'Global'
  1072. interfaces = device.interfaces.all()
  1073. self.fields['interface'].queryset = interfaces
  1074. self.fields['interface'].required = True
  1075. # If this device has only one interface, select it by default.
  1076. if len(interfaces) == 1:
  1077. self.fields['interface'].initial = interfaces[0]
  1078. # If this device does not have any IP addresses assigned, default to setting the first IP as its primary.
  1079. if not IPAddress.objects.filter(interface__device=device).count():
  1080. self.fields['set_as_primary'].initial = True
  1081. #
  1082. # Modules
  1083. #
  1084. class ModuleForm(BootstrapMixin, forms.ModelForm):
  1085. class Meta:
  1086. model = Module
  1087. fields = ['name', 'manufacturer', 'part_id', 'serial']