| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
- from django.contrib.contenttypes.models import ContentType
- from django.core.exceptions import ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator
- from django.db import models
- from django.urls import reverse
- from django.utils.translation import gettext_lazy as _
- from dcim.models import Interface
- from ipam.choices import *
- from ipam.constants import *
- from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
- from netbox.models import OrganizationalModel, PrimaryModel
- from virtualization.models import VMInterface
- __all__ = (
- 'VLAN',
- 'VLANGroup',
- )
- class VLANGroup(OrganizationalModel):
- """
- A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
- """
- name = models.CharField(
- verbose_name=_('name'),
- max_length=100
- )
- slug = models.SlugField(
- verbose_name=_('slug'),
- max_length=100
- )
- scope_type = models.ForeignKey(
- to=ContentType,
- on_delete=models.CASCADE,
- limit_choices_to=Q(model__in=VLANGROUP_SCOPE_TYPES),
- blank=True,
- null=True
- )
- scope_id = models.PositiveBigIntegerField(
- blank=True,
- null=True
- )
- scope = GenericForeignKey(
- ct_field='scope_type',
- fk_field='scope_id'
- )
- min_vid = models.PositiveSmallIntegerField(
- verbose_name=_('minimum VLAN ID'),
- default=VLAN_VID_MIN,
- validators=(
- MinValueValidator(VLAN_VID_MIN),
- MaxValueValidator(VLAN_VID_MAX)
- ),
- help_text=_('Lowest permissible ID of a child VLAN')
- )
- max_vid = models.PositiveSmallIntegerField(
- verbose_name=_('maximum VLAN ID'),
- default=VLAN_VID_MAX,
- validators=(
- MinValueValidator(VLAN_VID_MIN),
- MaxValueValidator(VLAN_VID_MAX)
- ),
- help_text=_('Highest permissible ID of a child VLAN')
- )
- objects = VLANGroupQuerySet.as_manager()
- class Meta:
- ordering = ('name', 'pk') # Name may be non-unique
- constraints = (
- models.UniqueConstraint(
- fields=('scope_type', 'scope_id', 'name'),
- name='%(app_label)s_%(class)s_unique_scope_name'
- ),
- models.UniqueConstraint(
- fields=('scope_type', 'scope_id', 'slug'),
- name='%(app_label)s_%(class)s_unique_scope_slug'
- ),
- )
- verbose_name = _('VLAN group')
- verbose_name_plural = _('VLAN groups')
- def get_absolute_url(self):
- return reverse('ipam:vlangroup', args=[self.pk])
- def clean(self):
- super().clean()
- # Validate scope assignment
- if self.scope_type and not self.scope_id:
- raise ValidationError(_("Cannot set scope_type without scope_id."))
- if self.scope_id and not self.scope_type:
- raise ValidationError(_("Cannot set scope_id without scope_type."))
- # Validate min/max child VID limits
- if self.max_vid < self.min_vid:
- raise ValidationError({
- 'max_vid': _("Maximum child VID must be greater than or equal to minimum child VID")
- })
- def get_available_vids(self):
- """
- Return all available VLANs within this group.
- """
- available_vlans = {vid for vid in range(self.min_vid, self.max_vid + 1)}
- available_vlans -= set(VLAN.objects.filter(group=self).values_list('vid', flat=True))
- return sorted(available_vlans)
- def get_next_available_vid(self):
- """
- Return the first available VLAN ID (1-4094) in the group.
- """
- available_vids = self.get_available_vids()
- if available_vids:
- return available_vids[0]
- return None
- def get_child_vlans(self):
- """
- Return all VLANs within this group.
- """
- return VLAN.objects.filter(group=self).order_by('vid')
- class VLAN(PrimaryModel):
- """
- A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
- to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
- within which all VLAN IDs and names but be unique.
- Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
- or more Prefixes assigned to it.
- """
- site = models.ForeignKey(
- to='dcim.Site',
- on_delete=models.PROTECT,
- related_name='vlans',
- blank=True,
- null=True,
- help_text=_("The specific site to which this VLAN is assigned (if any)")
- )
- group = models.ForeignKey(
- to='ipam.VLANGroup',
- on_delete=models.PROTECT,
- related_name='vlans',
- blank=True,
- null=True,
- help_text=_("VLAN group (optional)")
- )
- vid = models.PositiveSmallIntegerField(
- verbose_name=_('VLAN ID'),
- validators=(
- MinValueValidator(VLAN_VID_MIN),
- MaxValueValidator(VLAN_VID_MAX)
- ),
- help_text=_("Numeric VLAN ID (1-4094)")
- )
- name = models.CharField(
- verbose_name=_('name'),
- max_length=64
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='vlans',
- blank=True,
- null=True
- )
- status = models.CharField(
- verbose_name=_('status'),
- max_length=50,
- choices=VLANStatusChoices,
- default=VLANStatusChoices.STATUS_ACTIVE,
- help_text=_("Operational status of this VLAN")
- )
- role = models.ForeignKey(
- to='ipam.Role',
- on_delete=models.SET_NULL,
- related_name='vlans',
- blank=True,
- null=True,
- help_text=_("The primary function of this VLAN")
- )
- l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
- content_type_field='assigned_object_type',
- object_id_field='assigned_object_id',
- related_query_name='vlan'
- )
- objects = VLANQuerySet.as_manager()
- clone_fields = [
- 'site', 'group', 'tenant', 'status', 'role', 'description',
- ]
- class Meta:
- ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
- constraints = (
- models.UniqueConstraint(
- fields=('group', 'vid'),
- name='%(app_label)s_%(class)s_unique_group_vid'
- ),
- models.UniqueConstraint(
- fields=('group', 'name'),
- name='%(app_label)s_%(class)s_unique_group_name'
- ),
- )
- verbose_name = _('VLAN')
- verbose_name_plural = _('VLANs')
- def __str__(self):
- return f'{self.name} ({self.vid})'
- def get_absolute_url(self):
- return reverse('ipam:vlan', args=[self.pk])
- def clean(self):
- super().clean()
- # Validate VLAN group (if assigned)
- if self.group and self.site and self.group.scope != self.site:
- raise ValidationError({
- 'group': _(
- "VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
- ).format(group=self.group, scope=self.group.scope, site=self.site)
- })
- # Validate group min/max VIDs
- if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
- raise ValidationError({
- 'vid': _(
- "VID must be between {min_vid} and {max_vid} for VLANs in group {group}"
- ).format(min_vid=self.group.min_vid, max_vid=self.group.max_vid, group=self.group)
- })
- def get_status_color(self):
- return VLANStatusChoices.colors.get(self.status)
- def get_interfaces(self):
- # Return all device interfaces assigned to this VLAN
- return Interface.objects.filter(
- Q(untagged_vlan_id=self.pk) |
- Q(tagged_vlans=self.pk)
- ).distinct()
- def get_vminterfaces(self):
- # Return all VM interfaces assigned to this VLAN
- return VMInterface.objects.filter(
- Q(untagged_vlan_id=self.pk) |
- Q(tagged_vlans=self.pk)
- ).distinct()
- @property
- def l2vpn_termination(self):
- return self.l2vpn_terminations.first()
|