forms.py 43 KB

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