netbox-map 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netbox_map/__init__.py +23 -0
- netbox_map/api/__init__.py +0 -0
- netbox_map/api/serializers.py +115 -0
- netbox_map/api/urls.py +10 -0
- netbox_map/api/views.py +30 -0
- netbox_map/choices.py +44 -0
- netbox_map/filtersets.py +90 -0
- netbox_map/forms.py +449 -0
- netbox_map/migrations/0001_initial.py +133 -0
- netbox_map/migrations/0002_rename_netbox_map_assigne_idx_netbox_map__assigne_eb5b30_idx.py +18 -0
- netbox_map/migrations/0003_floorplantile_latitude_longitude_locationcoordinates.py +76 -0
- netbox_map/migrations/0004_mapmarker.py +125 -0
- netbox_map/migrations/0005_rename_netbox_map__assigne_mapmarker_idx_netbox_map__assigne_6fd3ec_idx.py +18 -0
- netbox_map/migrations/0006_mapsettings.py +28 -0
- netbox_map/migrations/0007_alter_mapsettings_id.py +18 -0
- netbox_map/migrations/__init__.py +0 -0
- netbox_map/models.py +464 -0
- netbox_map/navigation.py +67 -0
- netbox_map/search.py +29 -0
- netbox_map/signals.py +150 -0
- netbox_map/static/netbox_map/css/floorplan.css +706 -0
- netbox_map/static/netbox_map/css/leaflet-1.9.4.min.css +661 -0
- netbox_map/static/netbox_map/css/site_map.css +434 -0
- netbox_map/static/netbox_map/js/floorplan_editor.js +644 -0
- netbox_map/static/netbox_map/js/floorplan_viewer.js +1713 -0
- netbox_map/static/netbox_map/js/jspdf.umd.min.js +398 -0
- netbox_map/static/netbox_map/js/leaflet-1.9.4.min.js +6 -0
- netbox_map/static/netbox_map/js/site_map.js +1354 -0
- netbox_map/tables.py +135 -0
- netbox_map/template_content.py +41 -0
- netbox_map/templates/netbox_map/device_map_locations.html +62 -0
- netbox_map/templates/netbox_map/floorplan.html +74 -0
- netbox_map/templates/netbox_map/floorplan_visualization.html +560 -0
- netbox_map/templates/netbox_map/floorplantile.html +65 -0
- netbox_map/templates/netbox_map/inc/device_floorplan_panel.html +25 -0
- netbox_map/templates/netbox_map/inc/site_floorplan_panel.html +28 -0
- netbox_map/templates/netbox_map/mapmarker.html +82 -0
- netbox_map/templates/netbox_map/settings.html +135 -0
- netbox_map/templates/netbox_map/site_map.html +121 -0
- netbox_map/urls.py +43 -0
- netbox_map/views.py +586 -0
- netbox_map-0.1.0.dist-info/METADATA +418 -0
- netbox_map-0.1.0.dist-info/RECORD +46 -0
- netbox_map-0.1.0.dist-info/WHEEL +5 -0
- netbox_map-0.1.0.dist-info/licenses/LICENSE +190 -0
- netbox_map-0.1.0.dist-info/top_level.txt +1 -0
netbox_map/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from netbox.plugins import PluginConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MapConfig(PluginConfig):
|
|
5
|
+
name = 'netbox_map'
|
|
6
|
+
verbose_name = 'NetBox Map'
|
|
7
|
+
author = 'Christian Rose'
|
|
8
|
+
description = 'Interactive floor plan visualization for NetBox sites'
|
|
9
|
+
version = '0.1.0'
|
|
10
|
+
base_url = 'map'
|
|
11
|
+
min_version = '4.5.0'
|
|
12
|
+
default_settings = {
|
|
13
|
+
'default_grid_width': 20,
|
|
14
|
+
'default_grid_height': 20,
|
|
15
|
+
'default_tile_size': 60,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def ready(self):
|
|
19
|
+
super().ready()
|
|
20
|
+
from . import signals # noqa: F401
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
config = MapConfig
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
|
|
4
|
+
from dcim.api.serializers_.sites import SiteSerializer
|
|
5
|
+
from dcim.api.serializers import LocationSerializer
|
|
6
|
+
from netbox.api.fields import ContentTypeField
|
|
7
|
+
from netbox.api.serializers import NetBoxModelSerializer
|
|
8
|
+
from utilities.api import get_serializer_for_model
|
|
9
|
+
from ..models import FloorPlan, FloorPlanTile, LocationCoordinates, MapMarker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FloorPlanSerializer(NetBoxModelSerializer):
|
|
13
|
+
site = SiteSerializer(nested=True)
|
|
14
|
+
location = LocationSerializer(
|
|
15
|
+
nested=True,
|
|
16
|
+
required=False,
|
|
17
|
+
allow_null=True,
|
|
18
|
+
default=None
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class Meta:
|
|
22
|
+
model = FloorPlan
|
|
23
|
+
fields = [
|
|
24
|
+
'id', 'url', 'display_url', 'display', 'site', 'location',
|
|
25
|
+
'name', 'grid_width', 'grid_height', 'tile_size',
|
|
26
|
+
'background_image', 'description', 'comments',
|
|
27
|
+
'tags', 'custom_fields', 'created', 'last_updated',
|
|
28
|
+
]
|
|
29
|
+
brief_fields = ('id', 'url', 'display', 'name', 'site')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FloorPlanTileSerializer(NetBoxModelSerializer):
|
|
33
|
+
floorplan = FloorPlanSerializer(nested=True)
|
|
34
|
+
assigned_object_type = ContentTypeField(
|
|
35
|
+
queryset=ContentType.objects.filter(
|
|
36
|
+
app_label='dcim',
|
|
37
|
+
model__in=['device', 'rack', 'powerpanel', 'powerfeed'],
|
|
38
|
+
),
|
|
39
|
+
required=False,
|
|
40
|
+
allow_null=True,
|
|
41
|
+
)
|
|
42
|
+
assigned_object = serializers.SerializerMethodField(read_only=True)
|
|
43
|
+
utilization = serializers.SerializerMethodField(read_only=True)
|
|
44
|
+
|
|
45
|
+
class Meta:
|
|
46
|
+
model = FloorPlanTile
|
|
47
|
+
fields = [
|
|
48
|
+
'id', 'url', 'display_url', 'display', 'floorplan',
|
|
49
|
+
'x_position', 'y_position', 'width', 'height',
|
|
50
|
+
'assigned_object_type', 'assigned_object_id', 'assigned_object',
|
|
51
|
+
'label', 'tile_type', 'status', 'orientation',
|
|
52
|
+
'fov_direction', 'fov_angle', 'fov_distance',
|
|
53
|
+
'latitude', 'longitude',
|
|
54
|
+
'utilization',
|
|
55
|
+
'tags', 'custom_fields', 'created', 'last_updated',
|
|
56
|
+
]
|
|
57
|
+
brief_fields = (
|
|
58
|
+
'id', 'url', 'display', 'x_position', 'y_position',
|
|
59
|
+
'label', 'tile_type',
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_assigned_object(self, obj):
|
|
63
|
+
if obj.assigned_object is not None:
|
|
64
|
+
serializer = get_serializer_for_model(obj.assigned_object)
|
|
65
|
+
return serializer(obj.assigned_object, nested=True, context=self.context).data
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def get_utilization(self, obj):
|
|
69
|
+
if obj.assigned_object_type and obj.assigned_object_type.model == 'rack' and obj.assigned_object:
|
|
70
|
+
return round(obj.assigned_object.get_utilization(), 1)
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LocationCoordinatesSerializer(NetBoxModelSerializer):
|
|
75
|
+
location = LocationSerializer(nested=True)
|
|
76
|
+
|
|
77
|
+
class Meta:
|
|
78
|
+
model = LocationCoordinates
|
|
79
|
+
fields = [
|
|
80
|
+
'id', 'url', 'display_url', 'display', 'location',
|
|
81
|
+
'latitude', 'longitude',
|
|
82
|
+
'tags', 'custom_fields', 'created', 'last_updated',
|
|
83
|
+
]
|
|
84
|
+
brief_fields = ('id', 'url', 'display', 'location', 'latitude', 'longitude')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MapMarkerSerializer(NetBoxModelSerializer):
|
|
88
|
+
site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
|
|
89
|
+
assigned_object_type = ContentTypeField(
|
|
90
|
+
queryset=ContentType.objects.filter(
|
|
91
|
+
app_label='dcim',
|
|
92
|
+
model__in=['device', 'rack', 'powerpanel', 'powerfeed'],
|
|
93
|
+
),
|
|
94
|
+
required=False,
|
|
95
|
+
allow_null=True,
|
|
96
|
+
)
|
|
97
|
+
assigned_object = serializers.SerializerMethodField(read_only=True)
|
|
98
|
+
|
|
99
|
+
class Meta:
|
|
100
|
+
model = MapMarker
|
|
101
|
+
fields = [
|
|
102
|
+
'id', 'url', 'display_url', 'display',
|
|
103
|
+
'latitude', 'longitude', 'label', 'marker_type', 'status',
|
|
104
|
+
'site', 'fov_direction', 'fov_angle', 'fov_distance',
|
|
105
|
+
'assigned_object_type', 'assigned_object_id', 'assigned_object',
|
|
106
|
+
'description',
|
|
107
|
+
'tags', 'custom_fields', 'created', 'last_updated',
|
|
108
|
+
]
|
|
109
|
+
brief_fields = ('id', 'url', 'display', 'label', 'marker_type', 'latitude', 'longitude')
|
|
110
|
+
|
|
111
|
+
def get_assigned_object(self, obj):
|
|
112
|
+
if obj.assigned_object is not None:
|
|
113
|
+
serializer = get_serializer_for_model(obj.assigned_object)
|
|
114
|
+
return serializer(obj.assigned_object, nested=True, context=self.context).data
|
|
115
|
+
return None
|
netbox_map/api/urls.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from netbox.api.routers import NetBoxRouter
|
|
2
|
+
from . import views
|
|
3
|
+
|
|
4
|
+
router = NetBoxRouter()
|
|
5
|
+
router.register('floorplans', views.FloorPlanViewSet)
|
|
6
|
+
router.register('floorplan-tiles', views.FloorPlanTileViewSet)
|
|
7
|
+
router.register('location-coordinates', views.LocationCoordinatesViewSet)
|
|
8
|
+
router.register('map-markers', views.MapMarkerViewSet)
|
|
9
|
+
|
|
10
|
+
urlpatterns = router.urls
|
netbox_map/api/views.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from netbox.api.viewsets import NetBoxModelViewSet
|
|
2
|
+
from .. import filtersets
|
|
3
|
+
from ..models import FloorPlan, FloorPlanTile, LocationCoordinates, MapMarker
|
|
4
|
+
from .serializers import (
|
|
5
|
+
FloorPlanSerializer, FloorPlanTileSerializer, LocationCoordinatesSerializer,
|
|
6
|
+
MapMarkerSerializer,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FloorPlanViewSet(NetBoxModelViewSet):
|
|
11
|
+
queryset = FloorPlan.objects.all()
|
|
12
|
+
serializer_class = FloorPlanSerializer
|
|
13
|
+
filterset_class = filtersets.FloorPlanFilterSet
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FloorPlanTileViewSet(NetBoxModelViewSet):
|
|
17
|
+
queryset = FloorPlanTile.objects.select_related('floorplan', 'assigned_object_type')
|
|
18
|
+
serializer_class = FloorPlanTileSerializer
|
|
19
|
+
filterset_class = filtersets.FloorPlanTileFilterSet
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LocationCoordinatesViewSet(NetBoxModelViewSet):
|
|
23
|
+
queryset = LocationCoordinates.objects.select_related('location__site')
|
|
24
|
+
serializer_class = LocationCoordinatesSerializer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MapMarkerViewSet(NetBoxModelViewSet):
|
|
28
|
+
queryset = MapMarker.objects.select_related('site', 'assigned_object_type')
|
|
29
|
+
serializer_class = MapMarkerSerializer
|
|
30
|
+
filterset_class = filtersets.MapMarkerFilterSet
|
netbox_map/choices.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from netbox.choices import ChoiceSet
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FloorPlanTileTypeChoices(ChoiceSet):
|
|
6
|
+
TYPE_RACK = 'rack'
|
|
7
|
+
TYPE_AISLE = 'aisle'
|
|
8
|
+
TYPE_WALL = 'wall'
|
|
9
|
+
TYPE_COLUMN = 'column'
|
|
10
|
+
TYPE_DOOR = 'door'
|
|
11
|
+
TYPE_COOLING = 'cooling'
|
|
12
|
+
TYPE_POWER = 'power'
|
|
13
|
+
TYPE_EMPTY = 'empty'
|
|
14
|
+
TYPE_RESERVED = 'reserved'
|
|
15
|
+
TYPE_AP = 'ap'
|
|
16
|
+
TYPE_CAMERA = 'camera'
|
|
17
|
+
TYPE_PRINTER = 'printer'
|
|
18
|
+
|
|
19
|
+
CHOICES = [
|
|
20
|
+
(TYPE_RACK, _('Rack'), 'blue'),
|
|
21
|
+
(TYPE_AISLE, _('Aisle'), 'gray'),
|
|
22
|
+
(TYPE_WALL, _('Wall'), 'dark'),
|
|
23
|
+
(TYPE_COLUMN, _('Column'), 'dark'),
|
|
24
|
+
(TYPE_DOOR, _('Door'), 'teal'),
|
|
25
|
+
(TYPE_COOLING, _('Cooling'), 'cyan'),
|
|
26
|
+
(TYPE_POWER, _('Power'), 'yellow'),
|
|
27
|
+
(TYPE_EMPTY, _('Empty'), 'white'),
|
|
28
|
+
(TYPE_RESERVED, _('Reserved'), 'orange'),
|
|
29
|
+
(TYPE_AP, _('Access Point'), 'purple'),
|
|
30
|
+
(TYPE_CAMERA, _('Camera'), 'red'),
|
|
31
|
+
(TYPE_PRINTER, _('Printer'), 'orange'),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FloorPlanTileStatusChoices(ChoiceSet):
|
|
36
|
+
STATUS_ACTIVE = 'active'
|
|
37
|
+
STATUS_PLANNED = 'planned'
|
|
38
|
+
STATUS_DECOMMISSIONED = 'decommissioned'
|
|
39
|
+
|
|
40
|
+
CHOICES = [
|
|
41
|
+
(STATUS_ACTIVE, _('Active'), 'green'),
|
|
42
|
+
(STATUS_PLANNED, _('Planned'), 'cyan'),
|
|
43
|
+
(STATUS_DECOMMISSIONED, _('Decommissioned'), 'red'),
|
|
44
|
+
]
|
netbox_map/filtersets.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import django_filters
|
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
|
|
5
|
+
from dcim.models import Site, Location
|
|
6
|
+
from netbox.filtersets import NetBoxModelFilterSet
|
|
7
|
+
from .models import FloorPlan, FloorPlanTile, MapMarker
|
|
8
|
+
from .choices import FloorPlanTileTypeChoices, FloorPlanTileStatusChoices
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FloorPlanFilterSet(NetBoxModelFilterSet):
|
|
12
|
+
site_id = django_filters.ModelMultipleChoiceFilter(
|
|
13
|
+
queryset=Site.objects.all(),
|
|
14
|
+
label=_('Site (ID)'),
|
|
15
|
+
)
|
|
16
|
+
site = django_filters.ModelMultipleChoiceFilter(
|
|
17
|
+
field_name='site__slug',
|
|
18
|
+
queryset=Site.objects.all(),
|
|
19
|
+
to_field_name='slug',
|
|
20
|
+
label=_('Site (slug)'),
|
|
21
|
+
)
|
|
22
|
+
location_id = django_filters.ModelMultipleChoiceFilter(
|
|
23
|
+
queryset=Location.objects.all(),
|
|
24
|
+
label=_('Location (ID)'),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class Meta:
|
|
28
|
+
model = FloorPlan
|
|
29
|
+
fields = ['id', 'name']
|
|
30
|
+
|
|
31
|
+
def search(self, queryset, name, value):
|
|
32
|
+
return queryset.filter(name__icontains=value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FloorPlanTileFilterSet(NetBoxModelFilterSet):
|
|
36
|
+
floorplan_id = django_filters.ModelMultipleChoiceFilter(
|
|
37
|
+
queryset=FloorPlan.objects.all(),
|
|
38
|
+
label=_('Floor Plan (ID)'),
|
|
39
|
+
)
|
|
40
|
+
assigned_object_type = django_filters.ModelChoiceFilter(
|
|
41
|
+
queryset=ContentType.objects.filter(
|
|
42
|
+
app_label='dcim',
|
|
43
|
+
model__in=['device', 'rack', 'powerpanel', 'powerfeed'],
|
|
44
|
+
),
|
|
45
|
+
label=_('Object Type'),
|
|
46
|
+
)
|
|
47
|
+
tile_type = django_filters.MultipleChoiceFilter(
|
|
48
|
+
choices=FloorPlanTileTypeChoices,
|
|
49
|
+
label=_('Tile Type'),
|
|
50
|
+
)
|
|
51
|
+
status = django_filters.MultipleChoiceFilter(
|
|
52
|
+
choices=FloorPlanTileStatusChoices,
|
|
53
|
+
label=_('Status'),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
class Meta:
|
|
57
|
+
model = FloorPlanTile
|
|
58
|
+
fields = ['id', 'floorplan_id', 'assigned_object_type', 'tile_type', 'status',
|
|
59
|
+
'x_position', 'y_position']
|
|
60
|
+
|
|
61
|
+
def search(self, queryset, name, value):
|
|
62
|
+
return queryset.filter(label__icontains=value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MapMarkerFilterSet(NetBoxModelFilterSet):
|
|
66
|
+
site_id = django_filters.ModelMultipleChoiceFilter(
|
|
67
|
+
queryset=Site.objects.all(),
|
|
68
|
+
label=_('Site (ID)'),
|
|
69
|
+
)
|
|
70
|
+
site = django_filters.ModelMultipleChoiceFilter(
|
|
71
|
+
field_name='site__slug',
|
|
72
|
+
queryset=Site.objects.all(),
|
|
73
|
+
to_field_name='slug',
|
|
74
|
+
label=_('Site (slug)'),
|
|
75
|
+
)
|
|
76
|
+
marker_type = django_filters.MultipleChoiceFilter(
|
|
77
|
+
choices=FloorPlanTileTypeChoices,
|
|
78
|
+
label=_('Marker Type'),
|
|
79
|
+
)
|
|
80
|
+
status = django_filters.MultipleChoiceFilter(
|
|
81
|
+
choices=FloorPlanTileStatusChoices,
|
|
82
|
+
label=_('Status'),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
class Meta:
|
|
86
|
+
model = MapMarker
|
|
87
|
+
fields = ['id', 'site_id', 'marker_type', 'status']
|
|
88
|
+
|
|
89
|
+
def search(self, queryset, name, value):
|
|
90
|
+
return queryset.filter(label__icontains=value)
|