class Cluster(PrimaryModel): """ A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices. """ name = models.CharField(max_length=100, unique=True) type = models.ForeignKey(to=ClusterType, on_delete=models.PROTECT, related_name='clusters') group = models.ForeignKey(to=ClusterGroup, on_delete=models.PROTECT, related_name='clusters', blank=True, null=True) tenant = models.ForeignKey(to='tenancy.Tenant', on_delete=models.PROTECT, related_name='clusters', blank=True, null=True) site = models.ForeignKey(to='dcim.Site', on_delete=models.PROTECT, related_name='clusters', blank=True, null=True) comments = models.TextField(blank=True) vlan_groups = GenericRelation(to='ipam.VLANGroup', content_type_field='scope_type', object_id_field='scope_id', related_query_name='cluster') objects = RestrictedQuerySet.as_manager() clone_fields = [ 'type', 'group', 'tenant', 'site', ] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('virtualization:cluster', args=[self.pk]) def clean(self): super().clean() # If the Cluster is assigned to a Site, verify that all host Devices belong to that Site. if self.pk and self.site: nonsite_devices = Device.objects.filter(cluster=self).exclude( site=self.site).count() if nonsite_devices: raise ValidationError({ 'site': "{} devices are assigned as hosts for this cluster but are not in site {}" .format(nonsite_devices, self.site) })
class SecretRole(OrganizationalModel): """ A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles such as "Login Credentials" or "SNMP Communities." """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('secrets:secretrole', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.description, )
class DeviceRole(OrganizationalModel): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to virtual machines as well. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) color = ColorField(default=ColorChoices.COLOR_GREY) vm_role = models.BooleanField( default=True, verbose_name='VM Role', help_text='Virtual machines may be assigned to this role') description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('dcim:devicerole', args=[self.pk])
class Manufacturer(ChangeLoggedModel): """ A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. """ name = models.CharField(max_length=50, unique=True, verbose_name="Имя") slug = models.SlugField(unique=True) description = models.CharField(max_length=200, blank=True, verbose_name="Описание") objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] verbose_name = "Производитель" def __str__(self): return self.name def get_absolute_url(self): return "{}?manufacturer={}".format(reverse('dcim:devicetype_list'), self.slug) def to_csv(self): return (self.name, self.slug, self.description)
class Tenant(PrimaryModel): """ A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal department. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) group = models.ForeignKey(to='tenancy.TenantGroup', on_delete=models.SET_NULL, related_name='tenants', blank=True, null=True) description = models.CharField(max_length=200, blank=True) comments = models.TextField(blank=True) objects = RestrictedQuerySet.as_manager() clone_fields = [ 'group', 'description', ] class Meta: ordering = ['group', 'name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('tenancy:tenant', args=[self.pk])
class Tag(ChangeLoggedModel, TagBase): color = ColorField( default=ColorChoices.COLOR_GREY ) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'color', 'description'] class Meta: ordering = ['name'] def get_absolute_url(self): return reverse('extras:tag', args=[self.pk]) def slugify(self, tag, i=None): # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names) slug = slugify(tag, allow_unicode=True) if i is not None: slug += "_%d" % i return slug def to_csv(self): return ( self.name, self.slug, self.color, self.description )
class ObjectPermission(models.Model): """ A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects identified by ORM query parameters. """ name = models.CharField( max_length=100 ) description = models.CharField( max_length=200, blank=True ) enabled = models.BooleanField( default=True ) object_types = models.ManyToManyField( to=ContentType, limit_choices_to=Q( ~Q(app_label__in=['admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) | Q(app_label='auth', model__in=['group', 'user']) | Q(app_label='users', model__in=['objectpermission', 'token']) ), related_name='object_permissions' ) groups = models.ManyToManyField( to=Group, blank=True, related_name='object_permissions' ) users = models.ManyToManyField( to=User, blank=True, related_name='object_permissions' ) actions = ArrayField( base_field=models.CharField(max_length=30), help_text="The list of actions granted by this permission" ) constraints = models.JSONField( blank=True, null=True, help_text="Queryset filter matching the applicable objects of the selected type(s)" ) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['name'] verbose_name = "permission" def __str__(self): return self.name def list_constraints(self): """ Return all constraint sets as a list (even if only a single set is defined). """ if type(self.constraints) is not list: return [self.constraints] return self.constraints
class PowerPanel(PrimaryModel): """ A distribution point for electrical power; e.g. a data center RPP. """ site = models.ForeignKey(to='Site', on_delete=models.PROTECT) location = models.ForeignKey(to='dcim.Location', on_delete=models.PROTECT, blank=True, null=True) name = models.CharField(max_length=100) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['site', 'name'] unique_together = ['site', 'name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('dcim:powerpanel', args=[self.pk]) def clean(self): super().clean() # Location must belong to assigned Site if self.location and self.location.site != self.site: raise ValidationError( f"Location {self.location} ({self.location.site}) is in a different site than {self.site}" )
class JournalEntry(ChangeLoggedModel): """ A historical remark concerning an object; collectively, these form an object's journal. The journal is used to preserve historical context around an object, and complements NetBox's built-in change logging. For example, you might record a new journal entry when a device undergoes maintenance, or when a prefix is expanded. """ assigned_object_type = models.ForeignKey(to=ContentType, on_delete=models.CASCADE) assigned_object_id = models.PositiveIntegerField() assigned_object = GenericForeignKey(ct_field='assigned_object_type', fk_field='assigned_object_id') created = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey(to=User, on_delete=models.SET_NULL, blank=True, null=True) kind = models.CharField(max_length=30, choices=JournalEntryKindChoices, default=JournalEntryKindChoices.KIND_INFO) comments = models.TextField() objects = RestrictedQuerySet.as_manager() class Meta: ordering = ('-created', ) verbose_name_plural = 'journal entries' def __str__(self): return f"{date_format(self.created)} - {time_format(self.created)} ({self.get_kind_display()})" def get_absolute_url(self): return reverse('extras:journalentry', args=[self.pk]) def get_kind_class(self): return JournalEntryKindChoices.CSS_CLASSES.get(self.kind)
class CircuitTermination(PathEndpoint, CableTermination): circuit = models.ForeignKey(to='circuits.Circuit', on_delete=models.CASCADE, related_name='terminations') term_side = models.CharField(max_length=1, choices=CircuitTerminationSideChoices, verbose_name='Termination') site = models.ForeignKey(to='dcim.Site', on_delete=models.PROTECT, related_name='circuit_terminations') port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)', blank=True, null=True) upstream_speed = models.PositiveIntegerField( blank=True, null=True, verbose_name='Upstream speed (Kbps)', help_text='Upstream speed, if different from port speed') xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') description = models.CharField(max_length=200, blank=True) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side'] def __str__(self): return 'Side {}'.format(self.get_term_side_display()) def to_objectchange(self, action): # Annotate the parent Circuit try: related_object = self.circuit except Circuit.DoesNotExist: # Parent circuit has been deleted related_object = None return ObjectChange(changed_object=self, object_repr=str(self), action=action, related_object=related_object, object_data=serialize_object(self)) @property def parent(self): return self.circuit def get_peer_termination(self): peer_side = 'Z' if self.term_side == 'A' else 'A' try: return CircuitTermination.objects.prefetch_related('site').get( circuit=self.circuit, term_side=peer_side) except CircuitTermination.DoesNotExist: return None
class ClusterType(ChangeLoggedModel): """ A type of Cluster. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) description = models.CharField(max_length=200, blank=True) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return "{}?type={}".format(reverse('virtualization:cluster_list'), self.slug) def to_csv(self): return ( self.name, self.slug, self.description, )
class BGPBase(ChangeLoggedModel): """ """ site = models.ForeignKey(to='dcim.Site', on_delete=models.PROTECT, related_name="%(class)s_related", blank=True, null=True) tenant = models.ForeignKey(to='tenancy.Tenant', on_delete=models.PROTECT, blank=True, null=True) status = models.CharField(max_length=50, choices=ASNStatusChoices, default=ASNStatusChoices.STATUS_ACTIVE) role = models.ForeignKey(to='ipam.Role', on_delete=models.SET_NULL, blank=True, null=True) description = models.CharField(max_length=200, blank=True) tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() class Meta: abstract = True
class ProviderNetwork(PrimaryModel): """ This represents a provider network which exists outside of NetBox, the details of which are unknown or unimportant to the user. """ name = models.CharField(max_length=100) provider = models.ForeignKey(to='circuits.Provider', on_delete=models.PROTECT, related_name='networks') description = models.CharField(max_length=200, blank=True) comments = models.TextField(blank=True) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ('provider', 'name') constraints = (models.UniqueConstraint( fields=('provider', 'name'), name='circuits_providernetwork_provider_name'), ) unique_together = ('provider', 'name') def __str__(self): return self.name def get_absolute_url(self): return reverse('circuits:providernetwork', args=[self.pk])
class RouteTarget(PrimaryModel): """ A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364. """ name = models.CharField( max_length= VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4) unique=True, help_text='Route target value (formatted in accordance with RFC 4360)') description = models.CharField(max_length=200, blank=True) tenant = models.ForeignKey(to='tenancy.Tenant', on_delete=models.PROTECT, related_name='route_targets', blank=True, null=True) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'description', 'tenant'] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('ipam:routetarget', args=[self.pk]) def to_csv(self): return ( self.name, self.description, self.tenant.name if self.tenant else None, )
class CircuitType(ChangeLoggedModel): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named "Long Haul," "Metro," or "Out-of-Band". """ name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) def to_csv(self): return ( self.name, self.slug, self.description, )
class Config(models.Model): device = models.ForeignKey( to='dcim.Device', on_delete=models.PROTECT, ) files = models.FileField(upload_to='configs', blank=True, null=True) #date = models.TimeField(null=True) config_date = models.DateTimeField(null=True) session = models.CharField(max_length=100, null=True) config_type = models.CharField(max_length=100, null=True, choices=ConfigurationChoices) objects = RestrictedQuerySet.as_manager() def get_absolute_url(self): return reverse('dcim:config', kwargs={'pk': self.pk}) csv_headers = ['device', 'files', 'date', 'session'] class Meta: verbose_name = "Configuration Files Historie" ordering = []
class Role(OrganizationalModel): """ A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or "Management." """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) weight = models.PositiveSmallIntegerField(default=1000) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'weight', 'description'] class Meta: ordering = ['weight', 'name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('ipam:role', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.weight, self.description, )
class ClusterGroup(OrganizationalModel): """ An organizational group of Clusters. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) description = models.CharField(max_length=200, blank=True) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('virtualization:clustergroup', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.description, )
class RIR(OrganizationalModel): """ A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address space. This can be an organization like ARIN or RIPE, or a governing standard such as RFC 1918. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) is_private = models.BooleanField( default=False, verbose_name='Private', help_text='IP space managed by this RIR is considered private') description = models.CharField(max_length=200, blank=True) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'is_private', 'description'] class Meta: ordering = ['name'] verbose_name = 'RIR' verbose_name_plural = 'RIRs' def __str__(self): return self.name def get_absolute_url(self): return reverse('ipam:rir', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.is_private, self.description, )
class RackRole(OrganizationalModel): """ Racks can be organized by functional role, similar to Devices. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) color = ColorField(default=ColorChoices.COLOR_GREY) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'color', 'description'] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('dcim:rackrole', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.color, self.description, )
class CircuitType(OrganizationalModel): """ Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named "Long Haul," "Metro," or "Out-of-Band". """ name = models.CharField( max_length=100, unique=True ) slug = models.SlugField( max_length=100, unique=True ) description = models.CharField( max_length=200, blank=True, ) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('circuits:circuittype', args=[self.pk])
class Provider(ChangeLoggedModel, CustomFieldModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model stores information pertinent to the user's relationship with the Provider. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) asn = ASNField(blank=True, null=True, verbose_name='ASN', help_text='32-bit autonomous system number') account = models.CharField(max_length=30, blank=True, verbose_name='Account number') portal_url = models.URLField(blank=True, verbose_name='Portal URL') noc_contact = models.TextField(blank=True, verbose_name='NOC contact') admin_contact = models.TextField(blank=True, verbose_name='Admin contact') comments = models.TextField(blank=True) tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() csv_headers = [ 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', ] clone_fields = [ 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', ] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('circuits:provider', args=[self.slug]) def to_csv(self): return ( self.name, self.slug, self.asn, self.account, self.portal_url, self.noc_contact, self.admin_contact, self.comments, )
class ImageAttachment(BigIDModel): """ An uploaded image which is associated with an object. """ content_type = models.ForeignKey(to=ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() parent = GenericForeignKey(ct_field='content_type', fk_field='object_id') image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width') image_height = models.PositiveSmallIntegerField() image_width = models.PositiveSmallIntegerField() name = models.CharField(max_length=50, blank=True) created = models.DateTimeField(auto_now_add=True) objects = RestrictedQuerySet.as_manager() clone_fields = ('content_type', 'object_id') class Meta: ordering = ('name', 'pk') # name may be non-unique def __str__(self): if self.name: return self.name filename = self.image.name.rsplit('/', 1)[-1] return filename.split('_', 2)[2] def delete(self, *args, **kwargs): _name = self.image.name super().delete(*args, **kwargs) # Delete file from disk self.image.delete(save=False) # Deleting the file erases its name. We restore the image's filename here in case we still need to reference it # before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.) self.image.name = _name @property def size(self): """ Wrapper around `image.size` to suppress an OSError in case the file is inaccessible. Also opportunistically catch other exceptions that we know other storage back-ends to throw. """ expected_exceptions = [OSError] try: from botocore.exceptions import ClientError expected_exceptions.append(ClientError) except ImportError: pass try: return self.image.size except tuple(expected_exceptions): return None
class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel): """ Base model for all objects which support change logging. """ objects = RestrictedQuerySet.as_manager() class Meta: abstract = True
class RackReservation(PrimaryModel): """ One or more reserved units within a Rack. """ rack = models.ForeignKey(to='dcim.Rack', on_delete=models.CASCADE, related_name='reservations') units = ArrayField(base_field=models.PositiveSmallIntegerField()) tenant = models.ForeignKey(to='tenancy.Tenant', on_delete=models.PROTECT, related_name='rackreservations', blank=True, null=True) user = models.ForeignKey(to=User, on_delete=models.PROTECT) description = models.CharField(max_length=200) objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['created', 'pk'] def __str__(self): return "Reservation for rack {}".format(self.rack) def get_absolute_url(self): return reverse('dcim:rackreservation', args=[self.pk]) def clean(self): super().clean() if hasattr(self, 'rack') and self.units: # Validate that all specified units exist in the Rack. invalid_units = [u for u in self.units if u not in self.rack.units] if invalid_units: raise ValidationError({ 'units': "Invalid unit(s) for {}U rack: {}".format( self.rack.u_height, ', '.join([str(u) for u in invalid_units]), ), }) # Check that none of the units has already been reserved for this Rack. reserved_units = [] for resv in self.rack.reservations.exclude(pk=self.pk): reserved_units += resv.units conflicting_units = [u for u in self.units if u in reserved_units] if conflicting_units: raise ValidationError({ 'units': 'The following units have already been reserved: {}'. format(', '.join([str(u) for u in conflicting_units]), ) }) @property def unit_list(self): return array_to_string(self.units)
class ExportTemplate(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, limit_choices_to=FeatureQuery('export_templates')) name = models.CharField(max_length=100) description = models.CharField(max_length=200, blank=True) template_code = models.TextField( help_text= 'The list of objects being exported is passed as a context variable named <code>queryset</code>.' ) mime_type = models.CharField( max_length=50, blank=True, verbose_name='MIME type', help_text='Defaults to <code>text/plain</code>') file_extension = models.CharField( max_length=15, blank=True, help_text='Extension to append to the rendered filename') objects = RestrictedQuerySet.as_manager() class Meta: ordering = ['content_type', 'name'] unique_together = [['content_type', 'name']] def __str__(self): return '{}: {}'.format(self.content_type, self.name) def render(self, queryset): """ Render the contents of the template. """ context = {'queryset': queryset} output = render_jinja2(self.template_code, context) # Replace CRLF-style line terminators output = output.replace('\r\n', '\n') return output def render_to_response(self, queryset): """ Render the template to an HTTP response, delivered as a named file attachment """ output = self.render(queryset) mime_type = 'text/plain' if not self.mime_type else self.mime_type # Build the response response = HttpResponse(output, content_type=mime_type) filename = 'netbox_{}{}'.format( queryset.model._meta.verbose_name_plural, '.{}'.format(self.file_extension) if self.file_extension else '') response['Content-Disposition'] = 'attachment; filename="{}"'.format( filename) return response
class VirtualChassis(ChangeLoggedModel, CustomFieldModel): """ A collection of Devices which operate with a shared control plane (e.g. a switch stack). """ master = models.OneToOneField(to='Device', on_delete=models.PROTECT, related_name='vc_master_for', blank=True, null=True) name = models.CharField(max_length=64) domain = models.CharField(max_length=30, blank=True) tags = TaggableManager(through=TaggedItem) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'domain', 'master'] class Meta: ordering = ['name'] verbose_name_plural = 'virtual chassis' def __str__(self): return self.name def get_absolute_url(self): return reverse('dcim:virtualchassis', kwargs={'pk': self.pk}) def clean(self): super().clean() # Verify that the selected master device has been assigned to this VirtualChassis. (Skip when creating a new # VirtualChassis.) if self.pk and self.master and self.master not in self.members.all(): raise ValidationError({ 'master': f"The selected master ({self.master}) is not assigned to this virtual chassis." }) def delete(self, *args, **kwargs): # Check for LAG interfaces split across member chassis interfaces = Interface.objects.filter( device__in=self.members.all(), lag__isnull=False).exclude(lag__device=F('device')) if interfaces: raise ProtectedError( f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG", interfaces) return super().delete(*args, **kwargs) def to_csv(self): return ( self.name, self.domain, self.master.name if self.master else None, )
class VLANGroup(ChangeLoggedModel): """ A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. """ name = models.CharField( max_length=50 ) slug = models.SlugField() site = models.ForeignKey( to='dcim.Site', on_delete=models.PROTECT, related_name='vlan_groups', blank=True, null=True ) description = models.CharField( max_length=200, blank=True ) objects = RestrictedQuerySet.as_manager() csv_headers = ['name', 'slug', 'site', 'description'] class Meta: ordering = ('site', 'name', 'pk') # (site, name) may be non-unique unique_together = [ ['site', 'name'], ['site', '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_vlans', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.site.name if self.site else None, self.description, ) 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
class Platform(OrganizationalModel): """ Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". NetBox uses Platforms to determine how to interact with devices when pulling inventory data or other information by specifying a NAPALM driver. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) manufacturer = models.ForeignKey( to='dcim.Manufacturer', on_delete=models.PROTECT, related_name='platforms', blank=True, null=True, help_text= 'Optionally limit this platform to devices of a certain manufacturer') napalm_driver = models.CharField( max_length=50, blank=True, verbose_name='NAPALM driver', help_text= 'The name of the NAPALM driver to use when interacting with devices') napalm_args = models.JSONField( blank=True, null=True, verbose_name='NAPALM arguments', help_text= 'Additional arguments to pass when initiating the NAPALM driver (JSON format)' ) description = models.CharField(max_length=200, blank=True) objects = RestrictedQuerySet.as_manager() csv_headers = [ 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description' ] class Meta: ordering = ['name'] def __str__(self): return self.name def get_absolute_url(self): return reverse('dcim:platform', args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.manufacturer.name if self.manufacturer else None, self.napalm_driver, self.napalm_args, self.description, )
class ComponentModel(models.Model): """ An abstract model inherited by any model which has a parent Device. """ device = models.ForeignKey( to='dcim.Device', on_delete=models.CASCADE, related_name='%(class)ss' ) name = models.CharField( max_length=64 ) _name = NaturalOrderingField( target_field='name', max_length=100, blank=True ) label = models.CharField( max_length=64, blank=True, help_text="Physical label" ) description = models.CharField( max_length=200, blank=True ) objects = RestrictedQuerySet.as_manager() class Meta: abstract = True def __str__(self): if self.label: return f"{self.name} ({self.label})" return self.name def to_objectchange(self, action): # Annotate the parent Device try: device = self.device except ObjectDoesNotExist: # The parent Device has already been deleted device = None return ObjectChange( changed_object=self, object_repr=str(self), action=action, related_object=device, object_data=serialize_object(self) ) @property def parent(self): return getattr(self, 'device', None)