forms.py 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. import re
  2. from django import forms
  3. from django.db.models import Count, Q
  4. from ipam.models import IPAddress
  5. from utilities.forms import BootstrapMixin, SmallTextarea, SelectWithDisabled, ConfirmationForm, APISelect, \
  6. Livesearch, CSVDataField, CommentField, BulkImportForm, FlexibleModelChoiceField, ExpandableNameField
  7. from .models import Site, Rack, RackGroup, Device, Manufacturer, DeviceType, DeviceRole, Platform, ConsolePort, \
  8. ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, PowerPort, PowerPortTemplate, PowerOutlet, \
  9. PowerOutletTemplate, Interface, InterfaceTemplate, InterfaceConnection, CONNECTION_STATUS_CHOICES, \
  10. CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, IFACE_FF_VIRTUAL, STATUS_CHOICES
  11. BULK_STATUS_CHOICES = [
  12. ['', '---------'],
  13. ]
  14. BULK_STATUS_CHOICES += STATUS_CHOICES
  15. DEVICE_BY_PK_RE = '{\d+\}'
  16. def get_device_by_name_or_pk(name):
  17. """
  18. Attempt to retrieve a device by either its name or primary key ('{pk}').
  19. """
  20. if re.match(DEVICE_BY_PK_RE, name):
  21. pk = name.strip('{}')
  22. device = Device.objects.get(pk=pk)
  23. else:
  24. device = Device.objects.get(name=name)
  25. return device
  26. #
  27. # Sites
  28. #
  29. class SiteForm(forms.ModelForm, BootstrapMixin):
  30. comments = CommentField()
  31. class Meta:
  32. model = Site
  33. fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
  34. widgets = {
  35. 'physical_address': SmallTextarea(attrs={'rows': 3}),
  36. 'shipping_address': SmallTextarea(attrs={'rows': 3}),
  37. }
  38. help_texts = {
  39. 'name': "Full name of the site",
  40. 'slug': "URL-friendly unique shorthand (e.g. 'nyc3' for NYC3)",
  41. 'facility': "Data center provider and facility (e.g. Equinix NY7)",
  42. 'asn': "BGP autonomous system number",
  43. 'physical_address': "Physical location of the building (e.g. for GPS)",
  44. 'shipping_address': "If different from the physical address"
  45. }
  46. class SiteFromCSVForm(forms.ModelForm):
  47. class Meta:
  48. model = Site
  49. fields = ['name', 'slug', 'facility', 'asn']
  50. class SiteImportForm(BulkImportForm, BootstrapMixin):
  51. csv = CSVDataField(csv_form=SiteFromCSVForm)
  52. #
  53. # Rack groups
  54. #
  55. class RackGroupBulkDeleteForm(ConfirmationForm):
  56. pk = forms.ModelMultipleChoiceField(queryset=RackGroup.objects.all(), widget=forms.MultipleHiddenInput)
  57. def rackgroup_site_choices():
  58. site_choices = Site.objects.annotate(rack_count=Count('racks'))
  59. return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
  60. class RackGroupFilterForm(forms.Form, BootstrapMixin):
  61. site = forms.MultipleChoiceField(required=False, choices=rackgroup_site_choices,
  62. widget=forms.SelectMultiple(attrs={'size': 8}))
  63. #
  64. # Racks
  65. #
  66. class RackForm(forms.ModelForm, BootstrapMixin):
  67. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect(
  68. api_url='/api/dcim/rack-groups/?site_id={{site}}',
  69. ))
  70. comments = CommentField()
  71. class Meta:
  72. model = Rack
  73. fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
  74. help_texts = {
  75. 'site': "The site at which the rack exists",
  76. 'name': "Organizational rack name",
  77. 'facility_id': "The unique rack ID assigned by the facility",
  78. 'u_height': "Height in rack units",
  79. }
  80. widgets = {
  81. 'site': forms.Select(attrs={'filter-for': 'group'}),
  82. }
  83. def __init__(self, *args, **kwargs):
  84. super(RackForm, self).__init__(*args, **kwargs)
  85. # Limit rack group choices
  86. if self.is_bound and self.data.get('site'):
  87. self.fields['group'].queryset = RackGroup.objects.filter(site__pk=self.data['site'])
  88. elif self.initial.get('site'):
  89. self.fields['group'].queryset = RackGroup.objects.filter(site=self.initial['site'])
  90. else:
  91. self.fields['group'].choices = []
  92. class RackFromCSVForm(forms.ModelForm):
  93. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
  94. error_messages={'invalid_choice': 'Site not found.'})
  95. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, to_field_name='name',
  96. error_messages={'invalid_choice': 'Group not found.'})
  97. class Meta:
  98. model = Rack
  99. fields = ['site', 'group', 'name', 'facility_id', 'u_height']
  100. def clean(self):
  101. site = self.cleaned_data.get('site')
  102. group = self.cleaned_data.get('group')
  103. # Validate device type
  104. if site and group:
  105. try:
  106. self.instance.group = RackGroup.objects.get(site=site, name=group)
  107. except RackGroup.DoesNotExist:
  108. self.add_error('group', "Invalid rack group ({})".format(group))
  109. class RackImportForm(BulkImportForm, BootstrapMixin):
  110. csv = CSVDataField(csv_form=RackFromCSVForm)
  111. class RackBulkEditForm(forms.Form, BootstrapMixin):
  112. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  113. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  114. group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
  115. u_height = forms.IntegerField(required=False, label='Height (U)')
  116. comments = CommentField()
  117. class RackBulkDeleteForm(ConfirmationForm):
  118. pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
  119. def rack_site_choices():
  120. site_choices = Site.objects.annotate(rack_count=Count('racks'))
  121. return [(s.slug, '{} ({})'.format(s.name, s.rack_count)) for s in site_choices]
  122. def rack_group_choices():
  123. group_choices = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
  124. return [(g.slug, '{} > {} ({})'.format(g.site.name, g.name, g.rack_count)) for g in group_choices]
  125. class RackFilterForm(forms.Form, BootstrapMixin):
  126. site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
  127. widget=forms.SelectMultiple(attrs={'size': 8}))
  128. group = forms.MultipleChoiceField(required=False, choices=rack_group_choices,
  129. widget=forms.SelectMultiple(attrs={'size': 8}))
  130. #
  131. # Device types
  132. #
  133. class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
  134. class Meta:
  135. model = DeviceType
  136. fields = ['manufacturer', 'model', 'slug', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
  137. 'is_network_device']
  138. class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
  139. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  140. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
  141. u_height = forms.IntegerField(min_value=1, required=False)
  142. class DeviceTypeBulkDeleteForm(ConfirmationForm):
  143. pk = forms.ModelMultipleChoiceField(queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput)
  144. def devicetype_manufacturer_choices():
  145. manufacturer_choices = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
  146. return [(m.slug, '{} ({})'.format(m.name, m.devicetype_count)) for m in manufacturer_choices]
  147. class DeviceTypeFilterForm(forms.Form, BootstrapMixin):
  148. manufacturer = forms.MultipleChoiceField(required=False, choices=devicetype_manufacturer_choices,
  149. widget=forms.SelectMultiple(attrs={'size': 8}))
  150. #
  151. # Device component templates
  152. #
  153. class ConsolePortTemplateForm(forms.ModelForm, BootstrapMixin):
  154. name_pattern = ExpandableNameField(label='Name')
  155. class Meta:
  156. model = ConsolePortTemplate
  157. fields = ['name_pattern']
  158. class ConsoleServerPortTemplateForm(forms.ModelForm, BootstrapMixin):
  159. name_pattern = ExpandableNameField(label='Name')
  160. class Meta:
  161. model = ConsoleServerPortTemplate
  162. fields = ['name_pattern']
  163. class PowerPortTemplateForm(forms.ModelForm, BootstrapMixin):
  164. name_pattern = ExpandableNameField(label='Name')
  165. class Meta:
  166. model = PowerPortTemplate
  167. fields = ['name_pattern']
  168. class PowerOutletTemplateForm(forms.ModelForm, BootstrapMixin):
  169. name_pattern = ExpandableNameField(label='Name')
  170. class Meta:
  171. model = PowerOutletTemplate
  172. fields = ['name_pattern']
  173. class InterfaceTemplateForm(forms.ModelForm, BootstrapMixin):
  174. name_pattern = ExpandableNameField(label='Name')
  175. class Meta:
  176. model = InterfaceTemplate
  177. fields = ['name_pattern', 'form_factor', 'mgmt_only']
  178. #
  179. # Devices
  180. #
  181. class DeviceForm(forms.ModelForm, BootstrapMixin):
  182. site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'}))
  183. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect(
  184. api_url='/api/dcim/racks/?site_id={{site}}',
  185. display_field='display_name',
  186. attrs={'filter-for': 'position'}
  187. ))
  188. position = forms.TypedChoiceField(required=False, empty_value=None, widget=APISelect(
  189. api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}',
  190. disabled_indicator='device',
  191. ))
  192. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(),
  193. widget=forms.Select(attrs={'filter-for': 'device_type'}))
  194. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Model', widget=APISelect(
  195. api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
  196. display_field='model'
  197. ))
  198. comments = CommentField()
  199. class Meta:
  200. model = Device
  201. fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
  202. 'platform', 'primary_ip', 'ro_snmp', 'comments']
  203. help_texts = {
  204. 'device_role': "The function this device serves",
  205. 'serial': "Chassis serial number",
  206. 'ro_snmp': "Read-only SNMP string",
  207. }
  208. widgets = {
  209. 'face': forms.Select(attrs={'filter-for': 'position'}),
  210. 'manufacturer': forms.Select(attrs={'filter-for': 'device_type'}),
  211. }
  212. def __init__(self, *args, **kwargs):
  213. super(DeviceForm, self).__init__(*args, **kwargs)
  214. if self.instance.pk:
  215. # Initialize helper selections
  216. self.initial['site'] = self.instance.rack.site
  217. self.initial['manufacturer'] = self.instance.device_type.manufacturer
  218. # Compile list of IPs assigned to this device
  219. primary_ip_choices = []
  220. interface_ips = IPAddress.objects.filter(interface__device=self.instance)
  221. primary_ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
  222. nat_ips = IPAddress.objects.filter(nat_inside__interface__device=self.instance)\
  223. .select_related('nat_inside__interface')
  224. primary_ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
  225. self.fields['primary_ip'].choices = [(None, '---------')] + primary_ip_choices
  226. else:
  227. # An object that doesn't exist yet can't have any IPs assigned to it
  228. self.fields['primary_ip'].choices = []
  229. self.fields['primary_ip'].widget.attrs['readonly'] = True
  230. # Limit rack choices
  231. if self.is_bound:
  232. self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site'])
  233. elif self.initial.get('site'):
  234. self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site'])
  235. else:
  236. self.fields['rack'].choices = []
  237. # Rack position
  238. face = self.data.get('face')
  239. if face == '':
  240. face = None
  241. try:
  242. if self.is_bound and self.data.get('rack') and face is not None:
  243. position_choices = Rack.objects.get(pk=self.data['rack']).get_rack_units(face=face)
  244. elif self.initial.get('rack') and face is not None:
  245. position_choices = Rack.objects.get(pk=self.initial['rack']).get_rack_units(face=self.initial.get('face'))
  246. else:
  247. position_choices = []
  248. except Rack.DoesNotExist:
  249. position_choices = []
  250. self.fields['position'].choices = [('', '---------')] + [
  251. (p['id'], {
  252. 'label': p['name'],
  253. 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')),
  254. }) for p in position_choices
  255. ]
  256. # Limit device_type choices
  257. if self.is_bound:
  258. self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer__pk=self.data['manufacturer'])\
  259. .select_related('manufacturer')
  260. elif self.initial.get('manufacturer'):
  261. self.fields['device_type'].queryset = DeviceType.objects.filter(manufacturer=self.initial['manufacturer'])\
  262. .select_related('manufacturer')
  263. else:
  264. self.fields['device_type'].choices = []
  265. class DeviceFromCSVForm(forms.ModelForm):
  266. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
  267. error_messages={'invalid_choice': 'Invalid device role.'})
  268. manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
  269. error_messages={'invalid_choice': 'Invalid manufacturer.'})
  270. model_name = forms.CharField()
  271. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, to_field_name='name',
  272. error_messages={'invalid_choice': 'Invalid platform.'})
  273. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', error_messages={
  274. 'invalid_choice': 'Invalid site name.',
  275. })
  276. rack_name = forms.CharField()
  277. face = forms.ChoiceField(choices=[('front', 'Front'), ('rear', 'Rear')])
  278. class Meta:
  279. model = Device
  280. fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
  281. 'position', 'face']
  282. def clean(self):
  283. manufacturer = self.cleaned_data.get('manufacturer')
  284. model_name = self.cleaned_data.get('model_name')
  285. site = self.cleaned_data.get('site')
  286. rack_name = self.cleaned_data.get('rack_name')
  287. # Validate device type
  288. if manufacturer and model_name:
  289. try:
  290. self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
  291. except DeviceType.DoesNotExist:
  292. self.add_error('model_name', "Invalid device type ({})".format(model_name))
  293. # Validate rack
  294. if site and rack_name:
  295. try:
  296. self.instance.rack = Rack.objects.get(site=site, name=rack_name)
  297. except Rack.DoesNotExist:
  298. self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
  299. def clean_face(self):
  300. face = self.cleaned_data['face']
  301. if face.lower() == 'front':
  302. return 0
  303. if face.lower() == 'rear':
  304. return 1
  305. raise forms.ValidationError("Invalid rack face ({})".format(face))
  306. class DeviceImportForm(BulkImportForm, BootstrapMixin):
  307. csv = CSVDataField(csv_form=DeviceFromCSVForm)
  308. class DeviceBulkEditForm(forms.Form, BootstrapMixin):
  309. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  310. device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
  311. device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
  312. platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
  313. platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
  314. status = forms.ChoiceField(choices=BULK_STATUS_CHOICES, required=False, initial='', label='Status')
  315. serial = forms.CharField(max_length=50, required=False, label='Serial Number')
  316. ro_snmp = forms.CharField(max_length=50, required=False, label='SNMP (RO)')
  317. class DeviceBulkDeleteForm(ConfirmationForm):
  318. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  319. def device_site_choices():
  320. site_choices = Site.objects.annotate(device_count=Count('racks__devices'))
  321. return [(s.slug, '{} ({})'.format(s.name, s.device_count)) for s in site_choices]
  322. def device_role_choices():
  323. role_choices = DeviceRole.objects.annotate(device_count=Count('devices'))
  324. return [(r.slug, '{} ({})'.format(r.name, r.device_count)) for r in role_choices]
  325. def device_type_choices():
  326. type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
  327. return [(t.pk, '{} ({})'.format(t, t.device_count)) for t in type_choices]
  328. def device_platform_choices():
  329. platform_choices = Platform.objects.annotate(device_count=Count('devices'))
  330. return [(p.slug, '{} ({})'.format(p.name, p.device_count)) for p in platform_choices]
  331. class DeviceFilterForm(forms.Form, BootstrapMixin):
  332. site = forms.MultipleChoiceField(required=False, choices=device_site_choices,
  333. widget=forms.SelectMultiple(attrs={'size': 8}))
  334. role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
  335. widget=forms.SelectMultiple(attrs={'size': 8}))
  336. device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
  337. widget=forms.SelectMultiple(attrs={'size': 8}))
  338. platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
  339. #
  340. # Console ports
  341. #
  342. class ConsolePortForm(forms.ModelForm, BootstrapMixin):
  343. class Meta:
  344. model = ConsolePort
  345. fields = ['device', 'name']
  346. widgets = {
  347. 'device': forms.HiddenInput(),
  348. }
  349. class ConsolePortCreateForm(forms.Form, BootstrapMixin):
  350. name_pattern = ExpandableNameField(label='Name')
  351. class ConsoleConnectionCSVForm(forms.Form):
  352. console_server = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_console_server=True),
  353. to_field_name='name',
  354. error_messages={'invalid_choice': 'Console server not found'})
  355. cs_port = forms.CharField()
  356. device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  357. error_messages={'invalid_choice': 'Device not found'})
  358. console_port = forms.CharField()
  359. status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
  360. def clean(self):
  361. # Validate console server port
  362. if self.cleaned_data.get('console_server'):
  363. try:
  364. cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
  365. name=self.cleaned_data['cs_port'])
  366. if ConsolePort.objects.filter(cs_port=cs_port):
  367. raise forms.ValidationError("Console server port is already occupied (by {} {})"
  368. .format(cs_port.connected_console.device, cs_port.connected_console))
  369. except ConsoleServerPort.DoesNotExist:
  370. raise forms.ValidationError("Invalid console server port ({} {})"
  371. .format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
  372. # Validate console port
  373. if self.cleaned_data.get('device'):
  374. try:
  375. console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
  376. name=self.cleaned_data['console_port'])
  377. if console_port.cs_port:
  378. raise forms.ValidationError("Console port is already connected (to {} {})"
  379. .format(console_port.cs_port.device, console_port.cs_port))
  380. except ConsolePort.DoesNotExist:
  381. raise forms.ValidationError("Invalid console port ({} {})"
  382. .format(self.cleaned_data['device'], self.cleaned_data['console_port']))
  383. class ConsoleConnectionImportForm(BulkImportForm, BootstrapMixin):
  384. csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
  385. def clean(self):
  386. records = self.cleaned_data.get('csv')
  387. if not records:
  388. return
  389. connection_list = []
  390. for i, record in enumerate(records, start=1):
  391. form = self.fields['csv'].csv_form(data=record)
  392. if form.is_valid():
  393. console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
  394. name=form.cleaned_data['console_port'])
  395. console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
  396. name=form.cleaned_data['cs_port'])
  397. if form.cleaned_data['status'] == 'planned':
  398. console_port.connection_status = CONNECTION_STATUS_PLANNED
  399. else:
  400. console_port.connection_status = CONNECTION_STATUS_CONNECTED
  401. connection_list.append(console_port)
  402. else:
  403. for field, errors in form.errors.items():
  404. for e in errors:
  405. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  406. self.cleaned_data['csv'] = connection_list
  407. class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin):
  408. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  409. widget=forms.Select(attrs={'filter-for': 'console_server'}))
  410. console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False,
  411. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True',
  412. attrs={'filter-for': 'cs_port'}))
  413. livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch(
  414. query_key='q', query_url='dcim-api:device_list', field_to_update='console_server')
  415. )
  416. cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port',
  417. widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/',
  418. disabled_indicator='connected_console'))
  419. class Meta:
  420. model = ConsolePort
  421. fields = ['rack', 'console_server', 'livesearch', 'cs_port', 'connection_status']
  422. labels = {
  423. 'cs_port': 'Port',
  424. 'connection_status': 'Status',
  425. }
  426. def __init__(self, *args, **kwargs):
  427. super(ConsolePortConnectionForm, self).__init__(*args, **kwargs)
  428. if not self.instance.pk:
  429. raise RuntimeError("ConsolePortConnectionForm must be initialized with an existing ConsolePort instance.")
  430. self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
  431. self.fields['cs_port'].required = True
  432. self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
  433. # Initialize console server choices
  434. if self.is_bound and self.data.get('rack'):
  435. self.fields['console_server'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_console_server=True)
  436. elif self.initial.get('rack'):
  437. self.fields['console_server'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_console_server=True)
  438. else:
  439. self.fields['console_server'].choices = []
  440. # Initialize CS port choices
  441. if self.is_bound:
  442. self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.data['console_server'])
  443. elif self.initial.get('console_server', None):
  444. self.fields['cs_port'].queryset = ConsoleServerPort.objects.filter(device__pk=self.initial['console_server'])
  445. else:
  446. self.fields['cs_port'].choices = []
  447. #
  448. # Console server ports
  449. #
  450. class ConsoleServerPortForm(forms.ModelForm, BootstrapMixin):
  451. class Meta:
  452. model = ConsoleServerPort
  453. fields = ['device', 'name']
  454. widgets = {
  455. 'device': forms.HiddenInput(),
  456. }
  457. class ConsoleServerPortCreateForm(forms.Form, BootstrapMixin):
  458. name_pattern = ExpandableNameField(label='Name')
  459. class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin):
  460. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  461. widget=forms.Select(attrs={'filter-for': 'device'}))
  462. device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  463. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
  464. attrs={'filter-for': 'port'}))
  465. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  466. query_key='q', query_url='dcim-api:device_list', field_to_update='device')
  467. )
  468. port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port',
  469. widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/',
  470. disabled_indicator='cs_port'))
  471. connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
  472. widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
  473. class Meta:
  474. fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
  475. labels = {
  476. 'connection_status': 'Status',
  477. }
  478. def __init__(self, consoleserverport, *args, **kwargs):
  479. super(ConsoleServerPortConnectionForm, self).__init__(*args, **kwargs)
  480. self.fields['rack'].queryset = Rack.objects.filter(site=consoleserverport.device.rack.site)
  481. # Initialize device choices
  482. if self.is_bound and self.data.get('rack'):
  483. self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
  484. elif self.initial.get('rack', None):
  485. self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
  486. else:
  487. self.fields['device'].choices = []
  488. # Initialize port choices
  489. if self.is_bound:
  490. self.fields['port'].queryset = ConsolePort.objects.filter(device__pk=self.data['device'])
  491. elif self.initial.get('device', None):
  492. self.fields['port'].queryset = ConsolePort.objects.filter(device_pk=self.initial['device'])
  493. else:
  494. self.fields['port'].choices = []
  495. #
  496. # Power ports
  497. #
  498. class PowerPortForm(forms.ModelForm, BootstrapMixin):
  499. class Meta:
  500. model = PowerPort
  501. fields = ['device', 'name']
  502. widgets = {
  503. 'device': forms.HiddenInput(),
  504. }
  505. class PowerPortCreateForm(forms.Form, BootstrapMixin):
  506. name_pattern = ExpandableNameField(label='Name')
  507. class PowerConnectionCSVForm(forms.Form):
  508. pdu = FlexibleModelChoiceField(queryset=Device.objects.filter(device_type__is_pdu=True), to_field_name='name',
  509. error_messages={'invalid_choice': 'PDU not found.'})
  510. power_outlet = forms.CharField()
  511. device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  512. error_messages={'invalid_choice': 'Device not found'})
  513. power_port = forms.CharField()
  514. status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
  515. def clean(self):
  516. # Validate power outlet
  517. if self.cleaned_data.get('pdu'):
  518. try:
  519. power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
  520. name=self.cleaned_data['power_outlet'])
  521. if PowerPort.objects.filter(power_outlet=power_outlet):
  522. raise forms.ValidationError("Power outlet is already occupied (by {} {})"
  523. .format(power_outlet.connected_port.device,
  524. power_outlet.connected_port))
  525. except PowerOutlet.DoesNotExist:
  526. raise forms.ValidationError("Invalid PDU port ({} {})"
  527. .format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
  528. # Validate power port
  529. if self.cleaned_data.get('device'):
  530. try:
  531. power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
  532. name=self.cleaned_data['power_port'])
  533. if power_port.power_outlet:
  534. raise forms.ValidationError("Power port is already connected (to {} {})"
  535. .format(power_port.power_outlet.device, power_port.power_outlet))
  536. except PowerPort.DoesNotExist:
  537. raise forms.ValidationError("Invalid power port ({} {})"
  538. .format(self.cleaned_data['device'], self.cleaned_data['power_port']))
  539. class PowerConnectionImportForm(BulkImportForm, BootstrapMixin):
  540. csv = CSVDataField(csv_form=PowerConnectionCSVForm)
  541. def clean(self):
  542. records = self.cleaned_data.get('csv')
  543. if not records:
  544. return
  545. connection_list = []
  546. for i, record in enumerate(records, start=1):
  547. form = self.fields['csv'].csv_form(data=record)
  548. if form.is_valid():
  549. power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
  550. name=form.cleaned_data['power_port'])
  551. power_port.power_outlet = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
  552. name=form.cleaned_data['power_outlet'])
  553. if form.cleaned_data['status'] == 'planned':
  554. power_port.connection_status = CONNECTION_STATUS_PLANNED
  555. else:
  556. power_port.connection_status = CONNECTION_STATUS_CONNECTED
  557. connection_list.append(power_port)
  558. else:
  559. for field, errors in form.errors.items():
  560. for e in errors:
  561. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  562. self.cleaned_data['csv'] = connection_list
  563. class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin):
  564. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  565. widget=forms.Select(attrs={'filter-for': 'pdu'}))
  566. pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False,
  567. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True',
  568. attrs={'filter-for': 'power_outlet'}))
  569. livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch(
  570. query_key='q', query_url='dcim-api:device_list', field_to_update='pdu')
  571. )
  572. power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet',
  573. widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/',
  574. disabled_indicator='connected_port'))
  575. class Meta:
  576. model = PowerPort
  577. fields = ['rack', 'pdu', 'livesearch', 'power_outlet', 'connection_status']
  578. labels = {
  579. 'power_outlet': 'Outlet',
  580. 'connection_status': 'Status',
  581. }
  582. def __init__(self, *args, **kwargs):
  583. super(PowerPortConnectionForm, self).__init__(*args, **kwargs)
  584. if not self.instance.pk:
  585. raise RuntimeError("PowerPortConnectionForm must be initialized with an existing PowerPort instance.")
  586. self.fields['rack'].queryset = Rack.objects.filter(site=self.instance.device.rack.site)
  587. self.fields['power_outlet'].required = True
  588. self.fields['connection_status'].choices = CONNECTION_STATUS_CHOICES
  589. # Initialize PDU choices
  590. if self.is_bound and self.data.get('rack'):
  591. self.fields['pdu'].queryset = Device.objects.filter(rack=self.data['rack'], device_type__is_pdu=True)
  592. elif self.initial.get('rack', None):
  593. self.fields['pdu'].queryset = Device.objects.filter(rack=self.initial['rack'], device_type__is_pdu=True)
  594. else:
  595. self.fields['pdu'].choices = []
  596. # Initialize power outlet choices
  597. if self.is_bound:
  598. self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.data['pdu'])
  599. elif self.initial.get('pdu', None):
  600. self.fields['power_outlet'].queryset = PowerOutlet.objects.filter(device__pk=self.initial['pdu'])
  601. else:
  602. self.fields['power_outlet'].choices = []
  603. #
  604. # Power outlets
  605. #
  606. class PowerOutletForm(forms.ModelForm, BootstrapMixin):
  607. class Meta:
  608. model = PowerOutlet
  609. fields = ['device', 'name']
  610. widgets = {
  611. 'device': forms.HiddenInput(),
  612. }
  613. class PowerOutletCreateForm(forms.Form, BootstrapMixin):
  614. name_pattern = ExpandableNameField(label='Name')
  615. class PowerOutletConnectionForm(forms.Form, BootstrapMixin):
  616. rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  617. widget=forms.Select(attrs={'filter-for': 'device'}))
  618. device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  619. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}',
  620. attrs={'filter-for': 'port'}))
  621. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  622. query_key='q', query_url='dcim-api:device_list', field_to_update='device')
  623. )
  624. port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port',
  625. widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/',
  626. disabled_indicator='power_outlet'))
  627. connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status',
  628. widget=forms.Select(choices=CONNECTION_STATUS_CHOICES))
  629. class Meta:
  630. fields = ['rack', 'device', 'livesearch', 'port', 'connection_status']
  631. labels = {
  632. 'connection_status': 'Status',
  633. }
  634. def __init__(self, poweroutlet, *args, **kwargs):
  635. super(PowerOutletConnectionForm, self).__init__(*args, **kwargs)
  636. self.fields['rack'].queryset = Rack.objects.filter(site=poweroutlet.device.rack.site)
  637. # Initialize device choices
  638. if self.is_bound and self.data.get('rack'):
  639. self.fields['device'].queryset = Device.objects.filter(rack=self.data['rack'])
  640. elif self.initial.get('rack', None):
  641. self.fields['device'].queryset = Device.objects.filter(rack=self.initial['rack'])
  642. else:
  643. self.fields['device'].choices = []
  644. # Initialize port choices
  645. if self.is_bound:
  646. self.fields['port'].queryset = PowerPort.objects.filter(device__pk=self.data['device'])
  647. elif self.initial.get('device', None):
  648. self.fields['port'].queryset = PowerPort.objects.filter(device_pk=self.initial['device'])
  649. else:
  650. self.fields['port'].choices = []
  651. #
  652. # Interfaces
  653. #
  654. class InterfaceForm(forms.ModelForm, BootstrapMixin):
  655. class Meta:
  656. model = Interface
  657. fields = ['device', 'name', 'form_factor', 'mgmt_only', 'description']
  658. widgets = {
  659. 'device': forms.HiddenInput(),
  660. }
  661. class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
  662. name_pattern = ExpandableNameField(label='Name')
  663. class Meta:
  664. model = Interface
  665. fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
  666. class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
  667. pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
  668. #
  669. # Interface connections
  670. #
  671. class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin):
  672. interface_a = forms.ChoiceField(choices=[], widget=SelectWithDisabled, label='Interface')
  673. rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False,
  674. widget=forms.Select(attrs={'filter-for': 'device_b'}))
  675. device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False,
  676. widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}',
  677. attrs={'filter-for': 'interface_b'}))
  678. livesearch = forms.CharField(required=False, label='Device', widget=Livesearch(
  679. query_key='q', query_url='dcim-api:device_list', field_to_update='device_b')
  680. )
  681. interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface',
  682. widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical',
  683. disabled_indicator='is_connected'))
  684. class Meta:
  685. model = InterfaceConnection
  686. fields = ['interface_a', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status']
  687. def __init__(self, device_a, *args, **kwargs):
  688. super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
  689. self.fields['rack_b'].queryset = Rack.objects.filter(site=device_a.rack.site)
  690. # Initialize interface A choices
  691. device_a_interfaces = Interface.objects.filter(device=device_a).exclude(form_factor=IFACE_FF_VIRTUAL) \
  692. .select_related('circuit', 'connected_as_a', 'connected_as_b')
  693. self.fields['interface_a'].choices = [
  694. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces
  695. ]
  696. # Initialize device_b choices if rack_b is set
  697. if self.is_bound and self.data.get('rack_b'):
  698. self.fields['device_b'].queryset = Device.objects.filter(rack__pk=self.data['rack_b'])
  699. elif self.initial.get('rack_b'):
  700. self.fields['device_b'].queryset = Device.objects.filter(rack=self.initial['rack_b'])
  701. else:
  702. self.fields['device_b'].choices = []
  703. # Initialize interface_b choices if device_b is set
  704. if self.is_bound:
  705. device_b_interfaces = Interface.objects.filter(device=self.data['device_b']) \
  706. .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
  707. elif self.initial.get('device_b'):
  708. device_b_interfaces = Interface.objects.filter(device=self.initial['device_b']) \
  709. .exclude(form_factor=IFACE_FF_VIRTUAL).select_related('circuit', 'connected_as_a', 'connected_as_b')
  710. else:
  711. device_b_interfaces = []
  712. self.fields['interface_b'].choices = [
  713. (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_b_interfaces
  714. ]
  715. class InterfaceConnectionCSVForm(forms.Form):
  716. device_a = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  717. error_messages={'invalid_choice': 'Device A not found.'})
  718. interface_a = forms.CharField()
  719. device_b = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
  720. error_messages={'invalid_choice': 'Device B not found.'})
  721. interface_b = forms.CharField()
  722. status = forms.ChoiceField(choices=[('planned', 'Planned'), ('connected', 'Connected')])
  723. def clean(self):
  724. # Validate interface A
  725. if self.cleaned_data.get('device_a'):
  726. try:
  727. interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
  728. name=self.cleaned_data['interface_a'])
  729. except Interface.DoesNotExist:
  730. raise forms.ValidationError("Invalid interface ({} {})"
  731. .format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
  732. try:
  733. InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
  734. raise forms.ValidationError("{} {} is already connected"
  735. .format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
  736. except InterfaceConnection.DoesNotExist:
  737. pass
  738. # Validate interface B
  739. if self.cleaned_data.get('device_b'):
  740. try:
  741. interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
  742. name=self.cleaned_data['interface_b'])
  743. except Interface.DoesNotExist:
  744. raise forms.ValidationError("Invalid interface ({} {})"
  745. .format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
  746. try:
  747. InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
  748. raise forms.ValidationError("{} {} is already connected"
  749. .format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
  750. except InterfaceConnection.DoesNotExist:
  751. pass
  752. class InterfaceConnectionImportForm(BulkImportForm, BootstrapMixin):
  753. csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
  754. def clean(self):
  755. records = self.cleaned_data.get('csv')
  756. if not records:
  757. return
  758. connection_list = []
  759. for i, record in enumerate(records, start=1):
  760. form = self.fields['csv'].csv_form(data=record)
  761. if form.is_valid():
  762. interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
  763. name=form.cleaned_data['interface_a'])
  764. interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
  765. name=form.cleaned_data['interface_b'])
  766. connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
  767. if form.cleaned_data['status'] == 'planned':
  768. connection.connection_status = CONNECTION_STATUS_PLANNED
  769. else:
  770. connection.connection_status = CONNECTION_STATUS_CONNECTED
  771. connection_list.append(connection)
  772. else:
  773. for field, errors in form.errors.items():
  774. for e in errors:
  775. self.add_error('csv', "Record {} {}: {}".format(i, field, e))
  776. self.cleaned_data['csv'] = connection_list
  777. class InterfaceConnectionDeletionForm(forms.Form, BootstrapMixin):
  778. confirm = forms.BooleanField(required=True)
  779. # Used for HTTP redirect upon successful deletion
  780. device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
  781. #
  782. # Connections
  783. #
  784. class ConsoleConnectionFilterForm(forms.Form, BootstrapMixin):
  785. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  786. class PowerConnectionFilterForm(forms.Form, BootstrapMixin):
  787. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  788. class InterfaceConnectionFilterForm(forms.Form, BootstrapMixin):
  789. site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
  790. #
  791. # IP addresses
  792. #
  793. class IPAddressForm(forms.ModelForm, BootstrapMixin):
  794. set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
  795. class Meta:
  796. model = IPAddress
  797. fields = ['address', 'vrf', 'interface', 'set_as_primary']
  798. help_texts = {
  799. 'address': 'IPv4 or IPv6 address (with mask)'
  800. }
  801. def __init__(self, device, *args, **kwargs):
  802. super(IPAddressForm, self).__init__(*args, **kwargs)
  803. self.fields['vrf'].empty_label = 'Global'
  804. self.fields['interface'].queryset = device.interfaces.all()
  805. self.fields['interface'].required = True
  806. # If this device does not have any IP addresses assigned, default to setting the first IP as its primary
  807. if not IPAddress.objects.filter(interface__device=device).count():
  808. self.fields['set_as_primary'].initial = True