| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- from django.contrib.contenttypes.fields import GenericForeignKey
- 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 dcim.models import Interface
- from extras.utils import extras_features
- from ipam.choices import *
- from ipam.constants import *
- from ipam.querysets import VLANQuerySet
- from netbox.models import OrganizationalModel, PrimaryModel
- from utilities.querysets import RestrictedQuerySet
- from virtualization.models import VMInterface
- __all__ = (
- 'VLAN',
- 'VLANGroup',
- )
- @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
- class VLANGroup(OrganizationalModel):
- """
- A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
- """
- name = models.CharField(
- max_length=100
- )
- slug = models.SlugField(
- 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'
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- objects = RestrictedQuerySet.as_manager()
- class Meta:
- ordering = ('name', 'pk') # Name may be non-unique
- unique_together = [
- ['scope_type', 'scope_id', 'name'],
- ['scope_type', 'scope_id', 'slug'],
- ]
- verbose_name = 'VLAN group'
- verbose_name_plural = 'VLAN groups'
- def __str__(self):
- return self.name
- 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.")
- def get_next_available_vid(self):
- """
- Return the first available VLAN ID (1-4094) in the group.
- """
- vlan_ids = VLAN.objects.filter(group=self).values_list('vid', flat=True)
- for i in range(1, 4095):
- if i not in vlan_ids:
- return i
- return None
- @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
- 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
- )
- group = models.ForeignKey(
- to='ipam.VLANGroup',
- on_delete=models.PROTECT,
- related_name='vlans',
- blank=True,
- null=True
- )
- vid = models.PositiveSmallIntegerField(
- verbose_name='ID',
- validators=[MinValueValidator(1), MaxValueValidator(4094)]
- )
- name = models.CharField(
- max_length=64
- )
- tenant = models.ForeignKey(
- to='tenancy.Tenant',
- on_delete=models.PROTECT,
- related_name='vlans',
- blank=True,
- null=True
- )
- status = models.CharField(
- max_length=50,
- choices=VLANStatusChoices,
- default=VLANStatusChoices.STATUS_ACTIVE
- )
- role = models.ForeignKey(
- to='ipam.Role',
- on_delete=models.SET_NULL,
- related_name='vlans',
- blank=True,
- null=True
- )
- description = models.CharField(
- max_length=200,
- blank=True
- )
- 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
- unique_together = [
- ['group', 'vid'],
- ['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': f"VLAN is assigned to group {self.group} (scope: {self.group.scope}); cannot also assign to "
- f"site {self.site}."
- })
- def get_status_class(self):
- return VLANStatusChoices.CSS_CLASSES.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()
|