426 lines
14 KiB
Python
426 lines
14 KiB
Python
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from circuits.models import Circuit
|
|
from dcim.models import Device, DeviceRole, PowerPanel, PowerFeed
|
|
from extras.models import Tag
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models
|
|
from django.templatetags.static import static
|
|
from django.urls import reverse
|
|
from netbox.models import NetBoxModel
|
|
from netbox.models.features import (
|
|
ChangeLoggingMixin,
|
|
ExportTemplatesMixin,
|
|
EventRulesMixin,
|
|
)
|
|
|
|
from netbox_topology_views.utils import (
|
|
CONF_IMAGE_DIR,
|
|
IMAGE_DIR,
|
|
Role,
|
|
find_image_url,
|
|
get_model_role,
|
|
image_static_url,
|
|
)
|
|
|
|
from netbox_topology_views.choices import NodeLabelItems
|
|
|
|
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["content_type", "object_id"]),
|
|
]
|
|
|
|
objects: "models.Manager[RoleImage]"
|
|
|
|
image = models.CharField("Path within the netbox static directory", max_length=255)
|
|
|
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
object_id = models.PositiveIntegerField(null=True, blank=True)
|
|
|
|
__role: Optional[Role] = None
|
|
|
|
_netbox_private = True
|
|
|
|
@property
|
|
def role(self) -> Role:
|
|
if self.__role:
|
|
return self.__role
|
|
|
|
model_class = self.content_type.model_class()
|
|
|
|
if not model_class:
|
|
raise ValueError(f"Invalid content type: {self.content_type}")
|
|
|
|
if model_class == DeviceRole:
|
|
device_role: DeviceRole = DeviceRole.objects.get(pk=self.object_id)
|
|
self.__role = Role(slug=device_role.slug, name=device_role.name)
|
|
return self.__role
|
|
|
|
self.__role = get_model_role(model_class)
|
|
return self.__role
|
|
|
|
def __str__(self):
|
|
return f"{self.role} - {self.image}"
|
|
|
|
def get_image(self) -> Path:
|
|
"""Get Icon
|
|
|
|
returns the model's image's absolute path in the filesystem
|
|
raises ValueError if the file cannot be found
|
|
"""
|
|
path = Path(settings.STATIC_ROOT) / self.image
|
|
|
|
if not path.exists():
|
|
raise ValueError(f"{self.role} path '{path}' does not exists")
|
|
|
|
return path
|
|
|
|
def get_default_image(self, dir: Path = CONF_IMAGE_DIR):
|
|
"""Get default image
|
|
|
|
will attempt to find image in given directory with any file extension,
|
|
otherwise will try to find a `role-unknown` image
|
|
|
|
fallback is `STATIC_ROOT/netbox_topology_views/img/role-unknown.png`
|
|
"""
|
|
if url := find_image_url(self.role.slug, dir):
|
|
return url
|
|
|
|
# fallback to default role unknown image
|
|
return image_static_url(IMAGE_DIR / "role-unknown.png")
|
|
|
|
def get_image_url(self, dir: Path = CONF_IMAGE_DIR) -> str:
|
|
try:
|
|
self.get_image()
|
|
except ValueError:
|
|
return self.get_default_image(dir)
|
|
return static(f"/{self.image}")
|
|
|
|
class CoordinateGroup(NetBoxModel):
|
|
"""
|
|
A coordinate group is used to display the topology for a particular group.
|
|
This allows different visualizations with the same devices.
|
|
"""
|
|
name = models.CharField(
|
|
max_length=100,
|
|
unique=True,
|
|
)
|
|
|
|
description = models.CharField(
|
|
max_length=255,
|
|
blank = True,
|
|
)
|
|
|
|
_netbox_private = True
|
|
|
|
class Meta:
|
|
ordering = ['name']
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('plugins:netbox_topology_views:coordinategroup', args=[self.pk])
|
|
|
|
class Coordinate(NetBoxModel):
|
|
"""
|
|
Coordinates are being used to place devices in a topology view onto a certain
|
|
position. Devices belong to one or more coordinate groups. They have to
|
|
be unique together.
|
|
"""
|
|
device = models.ForeignKey(Device, on_delete=models.CASCADE)
|
|
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
|
|
|
x = models.IntegerField(
|
|
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
|
'Smaller values correspond to a position further to the left on the monitor.',
|
|
)
|
|
y = models.IntegerField(
|
|
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
|
'Smaller values correspond to a position further up on the monitor.',
|
|
)
|
|
|
|
_netbox_private = True
|
|
|
|
def get_or_create_default_group(group_id):
|
|
# Default group named "default" must always exist in order to make sure
|
|
# that coordinate values can be stored even if no coordinate group has been
|
|
# selected. The default group will be added automatically if it does not exist.
|
|
try:
|
|
if CoordinateGroup.objects.filter(name="default"):
|
|
group = CoordinateGroup.objects.get(name="default")
|
|
group_id = group.pk
|
|
else:
|
|
group = CoordinateGroup(
|
|
name="default",
|
|
description="Automatically generated default group. If you delete "
|
|
"this group, all default coordinates are gone for good but "
|
|
"the group itself will be re-created."
|
|
)
|
|
group.save()
|
|
group_id = group.pk
|
|
except:
|
|
return False
|
|
return group_id
|
|
|
|
class Meta:
|
|
ordering = ['group', 'device']
|
|
unique_together = ('device', 'group')
|
|
|
|
def __str__(self):
|
|
return f'{self.x};{self.y}'
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk])
|
|
|
|
class CircuitCoordinate(NetBoxModel):
|
|
"""
|
|
Coordinates are being used to place devices in a topology view onto a certain
|
|
position. Devices belong to one or more coordinate groups. They have to
|
|
be unique together.
|
|
"""
|
|
device = models.ForeignKey(Circuit, on_delete=models.CASCADE)
|
|
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
|
|
|
x = models.IntegerField(
|
|
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
|
'Smaller values correspond to a position further to the left on the monitor.',
|
|
)
|
|
y = models.IntegerField(
|
|
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
|
'Smaller values correspond to a position further up on the monitor.',
|
|
)
|
|
|
|
_netbox_private = True
|
|
|
|
def get_or_create_default_group(group_id):
|
|
# Default group named "default" must always exist in order to make sure
|
|
# that coordinate values can be stored even if no coordinate group has been
|
|
# selected. The default group will be added automatically if it does not exist.
|
|
try:
|
|
if CoordinateGroup.objects.filter(name="default"):
|
|
group = CoordinateGroup.objects.get(name="default")
|
|
group_id = group.pk
|
|
else:
|
|
group = CoordinateGroup(
|
|
name="default",
|
|
description="Automatically generated default group. If you delete "
|
|
"this group, all default coordinates are gone for good but "
|
|
"the group itself will be re-created."
|
|
)
|
|
group.save()
|
|
group_id = group.pk
|
|
except:
|
|
return False
|
|
return group_id
|
|
|
|
class Meta:
|
|
ordering = ['group', 'device']
|
|
unique_together = ('device', 'group')
|
|
|
|
def __str__(self):
|
|
return f'{self.x};{self.y}'
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('plugins:netbox_topology_views:circuitcoordinate', args=[self.pk])
|
|
|
|
class PowerPanelCoordinate(NetBoxModel):
|
|
"""
|
|
Coordinates are being used to place devices in a topology view onto a certain
|
|
position. Devices belong to one or more coordinate groups. They have to
|
|
be unique together.
|
|
"""
|
|
device = models.ForeignKey(PowerPanel, on_delete=models.CASCADE)
|
|
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
|
|
|
x = models.IntegerField(
|
|
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
|
'Smaller values correspond to a position further to the left on the monitor.',
|
|
)
|
|
y = models.IntegerField(
|
|
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
|
'Smaller values correspond to a position further up on the monitor.',
|
|
)
|
|
|
|
_netbox_private = True
|
|
|
|
def get_or_create_default_group(group_id):
|
|
# Default group named "default" must always exist in order to make sure
|
|
# that coordinate values can be stored even if no coordinate group has been
|
|
# selected. The default group will be added automatically if it does not exist.
|
|
try:
|
|
if CoordinateGroup.objects.filter(name="default"):
|
|
group = CoordinateGroup.objects.get(name="default")
|
|
group_id = group.pk
|
|
else:
|
|
group = CoordinateGroup(
|
|
name="default",
|
|
description="Automatically generated default group. If you delete "
|
|
"this group, all default coordinates are gone for good but "
|
|
"the group itself will be re-created."
|
|
)
|
|
group.save()
|
|
group_id = group.pk
|
|
except:
|
|
return False
|
|
return group_id
|
|
|
|
class Meta:
|
|
ordering = ['group', 'device']
|
|
unique_together = ('device', 'group')
|
|
|
|
def __str__(self):
|
|
return f'{self.x};{self.y}'
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('plugins:netbox_topology_views:powerpanelcoordinate', args=[self.pk])
|
|
|
|
class PowerFeedCoordinate(NetBoxModel):
|
|
"""
|
|
Coordinates are being used to place devices in a topology view onto a certain
|
|
position. Devices belong to one or more coordinate groups. They have to
|
|
be unique together.
|
|
"""
|
|
device = models.ForeignKey(PowerFeed, on_delete=models.CASCADE)
|
|
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
|
|
|
x = models.IntegerField(
|
|
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
|
'Smaller values correspond to a position further to the left on the monitor.',
|
|
)
|
|
y = models.IntegerField(
|
|
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
|
'Smaller values correspond to a position further up on the monitor.',
|
|
)
|
|
|
|
_netbox_private = True
|
|
|
|
def get_or_create_default_group(group_id):
|
|
# Default group named "default" must always exist in order to make sure
|
|
# that coordinate values can be stored even if no coordinate group has been
|
|
# selected. The default group will be added automatically if it does not exist.
|
|
try:
|
|
if CoordinateGroup.objects.filter(name="default"):
|
|
group = CoordinateGroup.objects.get(name="default")
|
|
group_id = group.pk
|
|
else:
|
|
group = CoordinateGroup(
|
|
name="default",
|
|
description="Automatically generated default group. If you delete "
|
|
"this group, all default coordinates are gone for good but "
|
|
"the group itself will be re-created."
|
|
)
|
|
group.save()
|
|
group_id = group.pk
|
|
except:
|
|
return False
|
|
return group_id
|
|
|
|
class Meta:
|
|
ordering = ['group', 'device']
|
|
unique_together = ('device', 'group')
|
|
|
|
def __str__(self):
|
|
return f'{self.x};{self.y}'
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('plugins:netbox_topology_views:powerfeedcoordinate', args=[self.pk])
|
|
|
|
class IndividualOptions(NetBoxModel):
|
|
CHOICES = (
|
|
('interface', 'interface'),
|
|
('frontport', 'frontport'),
|
|
('rearport', 'rearport'),
|
|
('poweroutlet', 'poweroutlet'),
|
|
('powerport', 'powerport'),
|
|
('consoleport', 'consoleport'),
|
|
('consoleserverport', 'consoleserverport'),
|
|
)
|
|
|
|
user_id = models.IntegerField(
|
|
null=True,
|
|
unique=True
|
|
)
|
|
ignore_cable_type = models.CharField(
|
|
max_length = 255,
|
|
blank = True,
|
|
)
|
|
preselected_device_roles = models.ManyToManyField(
|
|
to='dcim.DeviceRole',
|
|
related_name='+',
|
|
blank=True,
|
|
db_table='netbox_topology_views_individualoptions_preselected_device',
|
|
)
|
|
preselected_tags = models.ManyToManyField(
|
|
to='extras.Tag',
|
|
related_name='+',
|
|
blank=True,
|
|
db_table='netbox_topology_views_individualoptions_preselected_tag',
|
|
)
|
|
save_coords = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_unconnected = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_cables = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_logical_connections = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_single_cable_logical_conns = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_neighbors = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_circuit = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_power = models.BooleanField(
|
|
default=False
|
|
)
|
|
show_wireless = models.BooleanField(
|
|
default=False
|
|
)
|
|
group_sites = models.BooleanField(
|
|
default=False
|
|
)
|
|
group_locations = models.BooleanField(
|
|
default=False
|
|
)
|
|
group_racks = models.BooleanField(
|
|
default=False
|
|
)
|
|
group_virtualchassis = models.BooleanField(
|
|
default=False
|
|
)
|
|
draw_default_layout = models.BooleanField(
|
|
default=False
|
|
)
|
|
straight_cables = models.BooleanField(
|
|
default=False
|
|
)
|
|
draw_termination_labels = models.BooleanField(
|
|
default=False
|
|
)
|
|
draw_cable_labels = models.BooleanField(
|
|
default=False
|
|
)
|
|
grid_size = models.PositiveSmallIntegerField(
|
|
default=0
|
|
)
|
|
node_label_items = models.CharField(
|
|
max_length=255,
|
|
blank=True
|
|
)
|
|
|
|
_netbox_private = True
|
|
|
|
def __str___(self):
|
|
return f"{self.user_id}"
|