class DiscoveryResult(models.Model): discovery_request = models.ForeignKey( to=DiscoveryRequest, related_name='results', on_delete=models.CASCADE, ) device = models.ForeignKey( to=Device, related_name='+', null=True, blank=True, on_delete=models.SET_NULL, ) address = IPAddressField() status = models.PositiveSmallIntegerField( choices=RESULT_STATUS_CHOICES, default=RESULT_STATUS_TRYING ) def __str__(self): return '{} {}'.format(self.address, self.status) def get_absolute_url(self): return reverse('np_autodiscovery:discoveryresult', args=[self.pk]) def get_status_class(self): return RESULT_STATUS_CLASSES[self.status]
class ISPActiveDevice(ChangeLoggedModel): name = models.CharField(max_length=255) comments = models.TextField(null=True, blank=True) manufacturer = models.ForeignKey(Manufacturer, on_delete=models.PROTECT) device_type = models.ForeignKey(DeviceType, on_delete=models.PROTECT) site = models.ForeignKey(Site, on_delete=models.PROTECT) ip_address = IPAddressField(blank=True, null=True, default="") uuid = models.CharField(max_length=255) type = models.CharField(max_length=255, null=True, blank=True)
class BgpPeering(ChangeLoggedModel): site = models.ForeignKey(to="dcim.Site", on_delete=models.SET_NULL, blank=True, null=True) device = models.ForeignKey(to="dcim.Device", on_delete=models.PROTECT) local_ip = models.ForeignKey(to="ipam.IPAddress", on_delete=models.PROTECT) local_as = ASNField(help_text="32-bit ASN used locally") remote_ip = IPAddressField(help_text="IPv4 or IPv6 address (with mask)") remote_as = ASNField(help_text="32-bit ASN used by peer") peer_name = models.CharField(max_length=64, blank=True) description = models.CharField(max_length=200, blank=True)
class RadioAccessPoint(Equipment): ANTENNA_FREQUENCY_CHOICES = ( ("900mhz", "900"), ("2.4ghz", "2.4"), ("3.5ghz", "3.5"), ("5ghz", "5"), ) frequency = models.CharField(max_length=30, choices=ANTENNA_FREQUENCY_CHOICES) name = models.CharField(max_length=30) antenna = models.ForeignKey(AntennaProfile, on_delete=models.PROTECT) ip_address = IPAddressField(blank=True, null=True, default="") def get_absolute_url(self): return reverse("plugins:netbox_netisp:radioaccesspoint", args=[self.pk]) def __str__(self): return "{0}".format(self.name)
class IPAddress(PrimaryModel): """ An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface. Interfaces can have zero or more IPAddresses assigned to them. An IPAddress can also optionally point to a NAT inside IP, designating itself as a NAT outside IP. This is useful, for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP. """ address = IPAddressField(help_text='IPv4 or IPv6 address (with mask)') vrf = models.ForeignKey(to='ipam.VRF', on_delete=models.PROTECT, related_name='ip_addresses', blank=True, null=True, verbose_name='VRF') tenant = models.ForeignKey(to='tenancy.Tenant', on_delete=models.PROTECT, related_name='ip_addresses', blank=True, null=True) status = models.CharField(max_length=50, choices=IPAddressStatusChoices, default=IPAddressStatusChoices.STATUS_ACTIVE, help_text='The operational status of this IP') role = models.CharField(max_length=50, choices=IPAddressRoleChoices, blank=True, help_text='The functional role of this IP') assigned_object_type = models.ForeignKey( to=ContentType, limit_choices_to=IPADDRESS_ASSIGNMENT_MODELS, on_delete=models.PROTECT, related_name='+', blank=True, null=True) assigned_object_id = models.PositiveIntegerField(blank=True, null=True) assigned_object = GenericForeignKey(ct_field='assigned_object_type', fk_field='assigned_object_id') nat_inside = models.OneToOneField( to='self', on_delete=models.SET_NULL, related_name='nat_outside', blank=True, null=True, verbose_name='NAT (Inside)', help_text='The IP for which this address is the "outside" IP') dns_name = models.CharField( max_length=255, blank=True, validators=[DNSValidator], verbose_name='DNS Name', help_text='Hostname or FQDN (not case-sensitive)') description = models.CharField(max_length=200, blank=True) objects = IPAddressManager() csv_headers = [ 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', 'assigned_object_id', 'is_primary', 'dns_name', 'description', ] clone_fields = [ 'vrf', 'tenant', 'status', 'role', 'description', ] class Meta: ordering = ('address', 'pk') # address may be non-unique verbose_name = 'IP address' verbose_name_plural = 'IP addresses' def __str__(self): return str(self.address) def get_absolute_url(self): return reverse('ipam:ipaddress', args=[self.pk]) def get_duplicates(self): return IPAddress.objects.filter( vrf=self.vrf, address__net_host=str(self.address.ip)).exclude(pk=self.pk) def clean(self): super().clean() if self.address: # /0 masks are not acceptable if self.address.prefixlen == 0: raise ValidationError( {'address': "Cannot create IP address with /0 mask."}) # Enforce unique IP space (if applicable) if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or ( self.vrf and self.vrf.enforce_unique): duplicate_ips = self.get_duplicates() if duplicate_ips and ( self.role not in IPADDRESS_ROLES_NONUNIQUE or any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)): raise ValidationError({ 'address': "Duplicate IP address found in {}: {}".format( "VRF {}".format(self.vrf) if self.vrf else "global table", duplicate_ips.first(), ) }) # Check for primary IP assignment that doesn't match the assigned device/VM if self.pk: device = Device.objects.filter( Q(primary_ip4=self) | Q(primary_ip6=self)).first() if device: if getattr(self.assigned_object, 'device', None) != device: raise ValidationError({ 'interface': f"IP address is primary for device {device} but not assigned to it!" }) vm = VirtualMachine.objects.filter( Q(primary_ip4=self) | Q(primary_ip6=self)).first() if vm: if getattr(self.assigned_object, 'virtual_machine', None) != vm: raise ValidationError({ 'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to it!" }) # Validate IP status selection if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6: raise ValidationError( {'status': "Only IPv6 addresses can be assigned SLAAC status"}) def save(self, *args, **kwargs): # Force dns_name to lowercase self.dns_name = self.dns_name.lower() super().save(*args, **kwargs) def to_objectchange(self, action): # Annotate the assigned object, if any return super().to_objectchange(action, related_object=self.assigned_object) def to_csv(self): # Determine if this IP is primary for a Device is_primary = False if self.address.version == 4 and getattr(self, 'primary_ip4_for', False): is_primary = True elif self.address.version == 6 and getattr(self, 'primary_ip6_for', False): is_primary = True obj_type = None if self.assigned_object_type: obj_type = f'{self.assigned_object_type.app_label}.{self.assigned_object_type.model}' return ( self.address, self.vrf.name if self.vrf else None, self.tenant.name if self.tenant else None, self.get_status_display(), self.get_role_display(), obj_type, self.assigned_object_id, is_primary, self.dns_name, self.description, ) @property def family(self): if self.address: return self.address.version return None def _set_mask_length(self, value): """ Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly, e.g. for bulk editing. """ if self.address is not None: self.address.prefixlen = value mask_length = property(fset=_set_mask_length) def get_status_class(self): return IPAddressStatusChoices.CSS_CLASSES.get(self.status) def get_role_class(self): return IPAddressRoleChoices.CSS_CLASSES.get(self.role)
class CustomerPremiseEquipment(Equipment): ip_address = IPAddressField()
class IPRange(PrimaryModel): """ A range of IP addresses, defined by start and end addresses. """ start_address = IPAddressField( help_text='IPv4 or IPv6 address (with mask)') end_address = IPAddressField(help_text='IPv4 or IPv6 address (with mask)') size = models.PositiveIntegerField(editable=False) vrf = models.ForeignKey(to='ipam.VRF', on_delete=models.PROTECT, related_name='ip_ranges', blank=True, null=True, verbose_name='VRF') tenant = models.ForeignKey(to='tenancy.Tenant', on_delete=models.PROTECT, related_name='ip_ranges', blank=True, null=True) status = models.CharField(max_length=50, choices=IPRangeStatusChoices, default=IPRangeStatusChoices.STATUS_ACTIVE, help_text='Operational status of this range') role = models.ForeignKey(to='ipam.Role', on_delete=models.SET_NULL, related_name='ip_ranges', blank=True, null=True, help_text='The primary function of this range') description = models.CharField(max_length=200, blank=True) clone_fields = [ 'vrf', 'tenant', 'status', 'role', 'description', ] class Meta: ordering = (F('vrf').asc(nulls_first=True), 'start_address', 'pk' ) # (vrf, start_address) may be non-unique verbose_name = 'IP range' verbose_name_plural = 'IP ranges' def __str__(self): return self.name def get_absolute_url(self): return reverse('ipam:iprange', args=[self.pk]) def clean(self): super().clean() if self.start_address and self.end_address: # Check that start & end IP versions match if self.start_address.version != self.end_address.version: raise ValidationError({ 'end_address': f"Ending address version (IPv{self.end_address.version}) does not match starting " f"address (IPv{self.start_address.version})" }) # Check that the start & end IP prefix lengths match if self.start_address.prefixlen != self.end_address.prefixlen: raise ValidationError({ 'end_address': f"Ending address mask (/{self.end_address.prefixlen}) does not match starting " f"address mask (/{self.start_address.prefixlen})" }) # Check that the ending address is greater than the starting address if not self.end_address > self.start_address: raise ValidationError({ 'end_address': f"Ending address must be lower than the starting address ({self.start_address})" }) # Check for overlapping ranges overlapping_range = IPRange.objects.exclude(pk=self.pk).filter( vrf=self.vrf ).filter( Q(start_address__gte=self.start_address, start_address__lte=self.end_address) | # Starts inside Q(end_address__gte=self.start_address, end_address__lte=self.end_address) | # Ends inside Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside ).first() if overlapping_range: raise ValidationError( f"Defined addresses overlap with range {overlapping_range} in VRF {self.vrf}" ) # Validate maximum size MAX_SIZE = 2**32 - 1 if int(self.end_address.ip - self.start_address.ip) + 1 > MAX_SIZE: raise ValidationError( f"Defined range exceeds maximum supported size ({MAX_SIZE})" ) def save(self, *args, **kwargs): # Record the range's size (number of IP addresses) self.size = int(self.end_address.ip - self.start_address.ip) + 1 super().save(*args, **kwargs) @property def family(self): return self.start_address.version if self.start_address else None @property def range(self): return netaddr.IPRange(self.start_address.ip, self.end_address.ip) @property def mask_length(self): return self.start_address.prefixlen if self.start_address else None @cached_property def name(self): """ Return an efficient string representation of the IP range. """ separator = ':' if self.family == 6 else '.' start_chunks = str(self.start_address.ip).split(separator) end_chunks = str(self.end_address.ip).split(separator) base_chunks = [] for a, b in zip(start_chunks, end_chunks): if a == b: base_chunks.append(a) base_str = separator.join(base_chunks) start_str = separator.join(start_chunks[len(base_chunks):]) end_str = separator.join(end_chunks[len(base_chunks):]) return f'{base_str}{separator}{start_str}-{end_str}/{self.start_address.prefixlen}' def _set_prefix_length(self, value): """ Expose the IPRange object's prefixlen attribute on the parent model so that it can be manipulated directly, e.g. for bulk editing. """ self.start_address.prefixlen = value self.end_address.prefixlen = value prefix_length = property(fset=_set_prefix_length) def get_status_class(self): return IPRangeStatusChoices.CSS_CLASSES.get(self.status) def get_child_ips(self): """ Return all IPAddresses within this IPRange and VRF. """ return IPAddress.objects.filter(address__gte=self.start_address, address__lte=self.end_address, vrf=self.vrf) def get_available_ips(self): """ Return all available IPs within this range as an IPSet. """ range = netaddr.IPRange(self.start_address.ip, self.end_address.ip) child_ips = netaddr.IPSet( [ip.address.ip for ip in self.get_child_ips()]) return netaddr.IPSet(range) - child_ips @cached_property def first_available_ip(self): """ Return the first available IP within the range (or None). """ available_ips = self.get_available_ips() if not available_ips: return None return '{}/{}'.format(next(available_ips.__iter__()), self.start_address.prefixlen) @cached_property def utilization(self): """ Determine the utilization of the range and return it as a percentage. """ # Compile an IPSet to avoid counting duplicate IPs child_count = netaddr.IPSet( [ip.address.ip for ip in self.get_child_ips()]).size return int(float(child_count) / self.size * 100)