Skip to content

Commit

Permalink
Merge pull request #54 from felbinger/main
Browse files Browse the repository at this point in the history
Add documents for virtual machines
  • Loading branch information
jasonyates authored Apr 17, 2024
2 parents ac6191d + 316c42a commit 566b36b
Show file tree
Hide file tree
Showing 19 changed files with 515 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A plugin designed to faciliate the storage of site, circuit, device type and dev
- Device Types
- Sites
- Locations
- Virtual Machines

* Upload documents to your NetBox media/ folder or other Django supported storage method e.g. S3
* Supports a wide array of common file types (bmp, gif, jpeg, jpg, png, pdf, txt, doc, docx, xls, xlsx, xlsm)
Expand Down
2 changes: 2 additions & 0 deletions netbox_documents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ class NetboxDocuments(PluginConfig):
"enable_circuit_documents": True,
"enable_device_documents": True,
"enable_device_type_documents": True,
"enable_vm_documents": True,
"enable_navigation_menu": True,
"site_documents_location": "left",
"location_documents_location": "left",
"circuit_documents_location": "left",
"device_documents_location": "left",
"device_type_documents_location": "left",
"vm_documents_location": "left",
}

config = NetboxDocuments
32 changes: 31 additions & 1 deletion netbox_documents/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from rest_framework import serializers

from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from ..models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument
from ..models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument
from dcim.api.nested_serializers import NestedSiteSerializer, NestedLocationSerializer, NestedDeviceSerializer, NestedDeviceTypeSerializer
from circuits.api.nested_serializers import NestedCircuitSerializer
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .fields import UploadableBase64FileField

# Site Document Serializer
Expand Down Expand Up @@ -141,3 +142,32 @@ class Meta:
fields = (
'id', 'url', 'display', 'name', 'document', 'external_url', 'document_type', 'filename',
)

# VM Document Serializer
class VMDocumentSerializer(NetBoxModelSerializer):

url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_documents-api:vmdocument-detail'
)

vm = NestedVirtualMachineSerializer()
document = UploadableBase64FileField(required=False)

class Meta:
model = VMDocument
fields = (
'id', 'url', 'display', 'name', 'document', 'external_url', 'document_type', 'filename', 'vm', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
)

class NestedVMDocumentSerializer(WritableNestedSerializer):

url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_documents-api:vmdocument-detail'
)

class Meta:
model = VMDocument
fields = (
'id', 'url', 'display', 'name', 'document', 'external_url', 'document_type', 'filename',
)
1 change: 1 addition & 0 deletions netbox_documents/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
router.register('device-documents', views.DeviceDocumentViewSet)
router.register('device-type-documents', views.DeviceTypeDocumentViewSet)
router.register('circuit-documents', views.CircuitDocumentViewSet)
router.register('vm-documents', views.VMDocumentViewSet)

urlpatterns = router.urls
9 changes: 7 additions & 2 deletions netbox_documents/api/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from netbox.api.viewsets import NetBoxModelViewSet

from .. import models, filtersets
from .serializers import SiteDocumentSerializer, LocationDocumentSerializer, DeviceDocumentSerializer, DeviceTypeDocumentSerializer, CircuitDocumentSerializer
from .serializers import SiteDocumentSerializer, LocationDocumentSerializer, DeviceDocumentSerializer, DeviceTypeDocumentSerializer, CircuitDocumentSerializer, VMDocumentSerializer

class SiteDocumentViewSet(NetBoxModelViewSet):
queryset = models.SiteDocument.objects.prefetch_related('tags')
Expand All @@ -26,4 +26,9 @@ class DeviceTypeDocumentViewSet(NetBoxModelViewSet):
class CircuitDocumentViewSet(NetBoxModelViewSet):
queryset = models.CircuitDocument.objects.prefetch_related('tags')
serializer_class = CircuitDocumentSerializer
filterset_class = filtersets.CircuitDocumentFilterSet
filterset_class = filtersets.CircuitDocumentFilterSet

class VMDocumentViewSet(NetBoxModelViewSet):
queryset = models.VMDocument.objects.prefetch_related('tags')
serializer_class = VMDocumentSerializer
filterset_class = filtersets.VMDocumentFilterSet
16 changes: 15 additions & 1 deletion netbox_documents/filtersets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from netbox.filtersets import NetBoxModelFilterSet
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument
from django.db.models import Q

class SiteDocumentFilterSet(NetBoxModelFilterSet):
Expand Down Expand Up @@ -72,3 +72,17 @@ def search(self, queryset, name, value):
Q(name__icontains=value) |
Q(document__icontains=value)
)

class VMDocumentFilterSet(NetBoxModelFilterSet):

class Meta:
model = VMDocument
fields = ('id', 'name', 'document_type', 'vm')

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(document__icontains=value)
)
36 changes: 34 additions & 2 deletions netbox_documents/forms.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django import forms
from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
from dcim.models import Site, Location, Device, DeviceType
from dcim.models import Site, Location, Device, DeviceType
from virtualization.models import VirtualMachine
from circuits.models import Circuit
from utilities.forms.fields import TagFilterField, CommentField, DynamicModelChoiceField
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, CircuitDocTypeChoices, SiteDocTypeChoices, LocationDocTypeChoices, DeviceDocTypeChoices, DeviceTypeDocTypeChoices
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, CircuitDocTypeChoices, SiteDocTypeChoices, LocationDocTypeChoices, DeviceDocTypeChoices, DeviceTypeDocTypeChoices, VMDocument, VMDocTypeChoices


#### Site Document Form & Filter Form
Expand Down Expand Up @@ -179,3 +180,34 @@ class CircuitDocumentFilterForm(NetBoxModelFilterSetForm):
)

tag = TagFilterField(model)

#### VM Document Form & Filter Form
class VMDocumentForm(NetBoxModelForm):
comments = CommentField()

vm = DynamicModelChoiceField(
queryset=VirtualMachine.objects.all()
)

class Meta:
model = VMDocument
fields = ('name', 'document', 'external_url', 'document_type', 'vm', 'comments', 'tags')

class VMDocumentFilterForm(NetBoxModelFilterSetForm):
model = VMDocument

name = forms.CharField(
required=False
)

vm = forms.ModelMultipleChoiceField(
queryset=VirtualMachine.objects.all(),
required=False
)

document_type = forms.MultipleChoiceField(
choices=VMDocTypeChoices,
required=False
)

tag = TagFilterField(model)
40 changes: 40 additions & 0 deletions netbox_documents/migrations/0006_vmdocument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 4.2.9 on 2024-04-16 12:19

from django.db import migrations, models
import django.db.models.deletion
import netbox_documents.utils
import taggit.managers
import utilities.json


class Migration(migrations.Migration):

dependencies = [
('extras', '0106_bookmark_user_cascade_deletion'),
('virtualization', '0038_virtualdisk'),
('netbox_documents', '0005_alter_circuitdocument_external_url_and_more'),
]

operations = [
migrations.CreateModel(
name='VMDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('name', models.CharField(blank=True, max_length=100)),
('document', models.FileField(blank=True, upload_to=netbox_documents.utils.file_upload)),
('external_url', models.URLField(blank=True, max_length=255)),
('document_type', models.CharField(max_length=30)),
('comments', models.TextField(blank=True)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='virtualization.virtualmachine')),
],
options={
'verbose_name': 'VM Document',
'verbose_name_plural': 'VM Documents',
'ordering': ('name',),
},
),
]
119 changes: 119 additions & 0 deletions netbox_documents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ class CircuitDocTypeChoices(ChoiceSet):
('other', 'Other', 'gray'),
]

class VMDocTypeChoices(ChoiceSet):

key = 'DocTypeChoices.virtualmachine'

CHOICES = [
('diagram', 'Network Diagram', 'green'),
('manual', 'Manual', 'pink'),
('purchaseorder', 'Purchase Order', 'orange'),
('quote', 'Quote', 'indigo'),
('supportcontract', 'Support Contract', 'blue'),
('other', 'Other', 'gray'),
]

class SiteDocument(NetBoxModel):
name = models.CharField(
max_length=100,
Expand Down Expand Up @@ -618,3 +631,109 @@ def delete(self, *args, **kwargs):
else:
# Straight delete of external URL
super().delete(*args, **kwargs)


class VMDocument(NetBoxModel):
name = models.CharField(
max_length=100,
blank=True,
help_text='(Optional) Specify a name to display for this document. If no name is specified, the filename will be used.'
)

document = models.FileField(
upload_to=file_upload,
blank=True
)

external_url = models.URLField(
blank=True,
max_length=255
)

document_type = models.CharField(
max_length=30,
choices=VMDocTypeChoices
)

vm = models.ForeignKey(
to='virtualization.VirtualMachine',
on_delete=models.CASCADE,
related_name='documents'
)

comments = models.TextField(
blank=True
)

class Meta:
ordering = ('name',)
verbose_name_plural = "VM Documents"
verbose_name = "VM Document"

def get_document_type_color(self):
return VMDocTypeChoices.colors.get(self.document_type)

@property
def size(self):
"""
Wrapper around `document.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.document.size
except:
return None

@property
def filename(self):
if self.external_url:
return self.external_url
filename = self.document.name.rsplit('/', 1)[-1]
return filename.split('_', 1)[1]

def __str__(self):
if self.name:
return self.name

if self.external_url:
return self.external_url

filename = self.document.name.rsplit('/', 1)[-1]
return filename.split('_', 1)[1]

def get_absolute_url(self):
return reverse('plugins:netbox_documents:vmdocument', args=[self.pk])

def clean(self):
super().clean()

# Must have an uploaded document or an external URL. cannot have both
if not self.document and self.external_url == '':
raise ValidationError("A document must contain an uploaded file or an external URL.")
if self.document and self.external_url:
raise ValidationError("A document cannot contain both an uploaded file and an external URL.")

def delete(self, *args, **kwargs):

# Check if its a document or a URL
if self.external_url == '':

_name = self.document.name

# Delete file from disk
super().delete(*args, **kwargs)
self.document.delete(save=False)

# Restore the name of the document as it's re-used in the notifications later
self.document.name = _name
else:
# Straight delete of external URL
super().delete(*args, **kwargs)
15 changes: 15 additions & 0 deletions netbox_documents/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@
icon_class='mdi mdi-file-document-multiple'
)

# Add a menu item for VM Documents if enabled
if plugin_settings.get('enable_vm_documents'):
menuitem.append(
PluginMenuItem(
link='plugins:netbox_documents:vmdocument_list',
link_text='VM Documents',
buttons=[PluginMenuButton(
link='plugins:netbox_documents:vmdocument_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN
)]
)
)

else:

# Fall back to pre 3.4 navigation option
Expand Down
12 changes: 10 additions & 2 deletions netbox_documents/search.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from netbox.search import SearchIndex
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument
from .models import SiteDocument, LocationDocument, DeviceDocument, DeviceTypeDocument, CircuitDocument, VMDocument
from django.conf import settings

# If we run NB 3.4+ register search indexes
Expand Down Expand Up @@ -44,5 +44,13 @@ class DeviceDocumentIndex(SearchIndex):
("comments", 5000),
)

class VMDocumentIndex(SearchIndex):
model = VMDocument
fields = (
("name", 100),
("document", 500),
("comments", 5000),
)

# Register indexes
indexes = [SiteDocumentIndex, LocationDocumentIndex, CircuitDocumentIndex, DeviceTypeDocumentIndex, DeviceDocumentIndex]
indexes = [SiteDocumentIndex, LocationDocumentIndex, CircuitDocumentIndex, DeviceTypeDocumentIndex, DeviceDocumentIndex, VMDocumentIndex]
Loading

0 comments on commit 566b36b

Please sign in to comment.