common.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. from django import forms
  2. from django.db import transaction
  3. from django.utils.translation import gettext_lazy as _
  4. from netaddr import EUI, AddrFormatError
  5. from dcim.choices import *
  6. from dcim.constants import *
  7. from dcim.models import MACAddress
  8. from dcim.utils import get_module_bay_positions, resolve_module_placeholder
  9. from netbox.context import current_request
  10. from utilities.forms import get_field_value
  11. __all__ = (
  12. 'InterfaceCommonForm',
  13. 'ModuleCommonForm'
  14. )
  15. class InterfaceCommonForm(forms.Form):
  16. mtu = forms.IntegerField(
  17. required=False,
  18. min_value=INTERFACE_MTU_MIN,
  19. max_value=INTERFACE_MTU_MAX,
  20. label=_('MTU')
  21. )
  22. mac_address = forms.CharField(
  23. required=False,
  24. empty_value=None,
  25. label=_('MAC address'),
  26. help_text=_('Enter a MAC address to create and assign it as the primary MAC in one step.')
  27. )
  28. def __init__(self, *args, **kwargs):
  29. super().__init__(*args, **kwargs)
  30. # Determine the selected 802.1Q mode
  31. interface_mode = get_field_value(self, 'mode')
  32. # Delete VLAN tagging fields which are not relevant for the selected mode
  33. if interface_mode in (InterfaceModeChoices.MODE_ACCESS, InterfaceModeChoices.MODE_TAGGED_ALL):
  34. del self.fields['tagged_vlans']
  35. elif not interface_mode:
  36. del self.fields['vlan_group']
  37. del self.fields['untagged_vlan']
  38. del self.fields['tagged_vlans']
  39. if interface_mode != InterfaceModeChoices.MODE_Q_IN_Q:
  40. del self.fields['qinq_svlan']
  41. if self.instance and self.instance.pk and self.instance.primary_mac_address:
  42. # Pre-populate mac_address with the current primary MAC string so it round-trips cleanly
  43. self.fields['mac_address'].initial = str(self.instance.primary_mac_address.mac_address)
  44. def clean(self):
  45. super().clean()
  46. mac_address = self.cleaned_data.get('mac_address')
  47. if mac_address:
  48. try:
  49. EUI(mac_address, version=48)
  50. except (AddrFormatError, ValueError, TypeError):
  51. raise forms.ValidationError({
  52. 'mac_address': _('Enter a valid MAC address (e.g. 00:11:22:33:44:55).')
  53. })
  54. # Require add_macaddress permission when a MAC value is provided (it may need to be created).
  55. request = current_request.get()
  56. if request is not None and not request.user.has_perm('dcim.add_macaddress'):
  57. raise forms.ValidationError({
  58. 'mac_address': _('You do not have permission to create MAC addresses.')
  59. })
  60. parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine'
  61. if 'tagged_vlans' in self.fields.keys():
  62. tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \
  63. self.get_initial_for_field(self.fields['tagged_vlans'], 'tagged_vlans')
  64. else:
  65. tagged_vlans = []
  66. # Validate tagged VLANs; must be a global VLAN or in the same site
  67. if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
  68. valid_sites = [None, self.cleaned_data[parent_field].site]
  69. invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
  70. if invalid_vlans:
  71. raise forms.ValidationError({
  72. 'tagged_vlans': _(
  73. "The tagged VLANs ({vlans}) must belong to the same site as the interface's parent device/VM, "
  74. "or they must be global"
  75. ).format(vlans=', '.join(invalid_vlans))
  76. })
  77. # Validate mode change
  78. if self.instance.pk and (self.instance.mode != self.cleaned_data['mode']):
  79. if 'untagged_vlan' not in self.cleaned_data and self.instance.untagged_vlan is not None:
  80. self.instance.untagged_vlan = None
  81. if 'tagged_vlans' not in self.cleaned_data and self.instance.tagged_vlans is not None:
  82. self.instance.tagged_vlans.clear()
  83. def save(self, commit=True):
  84. instance = super().save(commit=commit)
  85. if not commit or 'mac_address' not in self.changed_data:
  86. return instance
  87. mac_address = self.cleaned_data.get('mac_address')
  88. with transaction.atomic():
  89. if mac_address:
  90. # Find an existing MACAddress on this interface with the target value, or create one.
  91. # Using find-or-create avoids duplicating a MAC that already exists on this interface.
  92. mac = instance.mac_addresses.filter(mac_address=mac_address).first()
  93. if mac is None:
  94. mac = MACAddress(mac_address=mac_address, assigned_object=instance)
  95. mac.save()
  96. if instance.primary_mac_address_id != mac.pk:
  97. instance.snapshot()
  98. instance.primary_mac_address = mac
  99. instance.save()
  100. else:
  101. if instance.primary_mac_address_id is not None:
  102. instance.snapshot()
  103. instance.primary_mac_address = None
  104. instance.save()
  105. instance.__dict__.pop('mac_address', None)
  106. return instance
  107. class ModuleCommonForm(forms.Form):
  108. def clean(self):
  109. super().clean()
  110. replicate_components = self.cleaned_data.get('replicate_components')
  111. adopt_components = self.cleaned_data.get('adopt_components')
  112. device = self.cleaned_data.get('device')
  113. module_type = self.cleaned_data.get('module_type')
  114. module_bay = self.cleaned_data.get('module_bay')
  115. if adopt_components:
  116. self.instance._adopt_components = True
  117. # Bail out if we are not installing a new module or if we are not replicating components (or if
  118. # validation has already failed)
  119. if self.errors or self.instance.pk or not replicate_components:
  120. self.instance._disable_replication = True
  121. return
  122. try:
  123. positions = get_module_bay_positions(module_bay)
  124. except ValueError as e:
  125. raise forms.ValidationError(str(e))
  126. for templates, component_attribute in [
  127. ("consoleporttemplates", "consoleports"),
  128. ("consoleserverporttemplates", "consoleserverports"),
  129. ("interfacetemplates", "interfaces"),
  130. ("powerporttemplates", "powerports"),
  131. ("poweroutlettemplates", "poweroutlets"),
  132. ("rearporttemplates", "rearports"),
  133. ("frontporttemplates", "frontports")
  134. ]:
  135. # Prefetch installed components
  136. installed_components = {
  137. component.name: component for component in getattr(device, component_attribute).all()
  138. }
  139. # Get the templates for the module type.
  140. for template in getattr(module_type, templates).all():
  141. resolved_name = template.name
  142. if MODULE_TOKEN in template.name:
  143. if not module_bay.position:
  144. raise forms.ValidationError(
  145. _("Cannot install module with placeholder values in a module bay with no position defined.")
  146. )
  147. try:
  148. resolved_name = resolve_module_placeholder(template.name, positions)
  149. except ValueError as e:
  150. raise forms.ValidationError(str(e))
  151. existing_item = installed_components.get(resolved_name)
  152. # It is not possible to adopt components already belonging to a module
  153. if adopt_components and existing_item and existing_item.module:
  154. raise forms.ValidationError(
  155. _("Cannot adopt {model} {name} as it already belongs to a module").format(
  156. model=template.component_model.__name__,
  157. name=resolved_name
  158. )
  159. )
  160. # If we are not adopting components we error if the component exists
  161. if not adopt_components and resolved_name in installed_components:
  162. raise forms.ValidationError(
  163. _("A {model} named {name} already exists").format(
  164. model=template.component_model.__name__,
  165. name=resolved_name
  166. )
  167. )