├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── netbox_storage ├── __init__.py ├── api │ ├── __init__.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── filtersets.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── navigation.py ├── tables.py ├── template_content.py ├── templates │ └── netbox_storage │ │ ├── datastore.html │ │ ├── lun.html │ │ ├── storagepool.html │ │ ├── storagesession.html │ │ ├── vm_vmdk_extend.html │ │ └── vmdk.html ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.egg-info/ 3 | __pycache__/ 4 | build/ 5 | venv/ 6 | dist/ 7 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include netbox_storage/templates *.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netbox-storage 2 | 3 | A [Netbox](https://github.com/netbox-community/netbox) plugin for storage related documentation where virtualization is used. 4 | 5 | 5 new object types are introduced: 6 | - Storage Pools: a pool that is created on a storage. Currently this storage has to be a Netbox Device. 7 | - LUN: tied to a Storage Pool 8 | - Datastores: created on LUN(s) 9 | - Storage Sessions: the "source" of a session is a Netbox Virtualization Cluster, the "destination" is the LUN Group 10 | - VMDK: can be assigned to a VM and a datastore 11 | 12 | # Install 13 | 14 | The plugin can be installed using pip: 15 | 16 | ``` 17 | pip install netbox-storage-plugin 18 | ``` 19 | Add netbox_storage to PLUGINS in configuration.py: 20 | ``` 21 | PLUGINS = ['netbox_storage',] 22 | ``` 23 | 24 | Don't forget to add ```netbox-storage-plugin``` to your local_requirements.txt as well. 25 | 26 | # Usage 27 | 28 | 1. Create regular Netbox objects: a storage Device, a virtualization Cluster, and a Virtual Machine 29 | 2. Create a Storage Pool that is assigned to the above created Device 30 | 3. Create LUN(s) on the Storage Pool 31 | 4. Create Datastore(s) on LUNs 32 | 5. Create Storage Session between the Cluster and the Datastore 33 | 6. Create VMDK on the VM that is on a Cluster that has a Storage Session: this is possible either from the main menu, or on the VM's own page 34 | -------------------------------------------------------------------------------- /netbox_storage/__init__.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginConfig 2 | 3 | class NetBoxStorageConfig(PluginConfig): 4 | name = 'netbox_storage' 5 | verbose_name = ' NetBox Storage' 6 | description = 'Netbox Storage Administration Plugin' 7 | version = '0.8.0' 8 | base_url = 'storage' 9 | min_version = "4.3.0" 10 | author = 'Gabor Somogyvari' 11 | 12 | 13 | config = NetBoxStorageConfig 14 | -------------------------------------------------------------------------------- /netbox_storage/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viroge/netbox-storage/2366cd9c717c234d17de3936bc2206265a769805/netbox_storage/api/__init__.py -------------------------------------------------------------------------------- /netbox_storage/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from virtualization.api.serializers import ClusterSerializer, VirtualMachineSerializer 4 | from dcim.api.serializers import DeviceSerializer 5 | from netbox.api.serializers import NetBoxModelSerializer 6 | from netbox.api.fields import SerializedPKRelatedField 7 | from ..models import StoragePool, LUN, StorageSession, Datastore, VMDK 8 | 9 | 10 | class StoragePoolSerializer(NetBoxModelSerializer): 11 | url = serializers.HyperlinkedIdentityField( 12 | view_name='plugins-api:netbox_storage-api:storagepool-detail' 13 | ) 14 | device = DeviceSerializer(nested=True) 15 | 16 | class Meta: 17 | model = StoragePool 18 | fields = ( 19 | 'id', 'url', 'display', 'name', 'size', 'device', 'description', 20 | 'tags', 'custom_fields', 'created', 'last_updated', 21 | ) 22 | brief_fields = ( 23 | 'id', 'url', 'display', 'name', 'size', 'device', 24 | ) 25 | 26 | 27 | class LUNSerializer(NetBoxModelSerializer): 28 | url = serializers.HyperlinkedIdentityField( 29 | view_name='plugins-api:netbox_storage-api:lun-detail' 30 | ) 31 | storage_pool = StoragePoolSerializer(nested=True) 32 | 33 | class Meta: 34 | model = LUN 35 | fields = ( 36 | 'id', 'url', 'display', 'name', 'size', 'storage_pool', 'wwn', 37 | 'description', 'tags', 'custom_fields', 38 | 'created', 'last_updated', 39 | ) 40 | brief_fields = ( 41 | 'id', 'url', 'display', 'name', 'size', 'storage_pool', 42 | ) 43 | 44 | 45 | class DatastoreSerializer(NetBoxModelSerializer): 46 | url = serializers.HyperlinkedIdentityField( 47 | view_name='plugins-api:netbox_storage-api:datastore-detail' 48 | ) 49 | lun = SerializedPKRelatedField( 50 | queryset=LUN.objects.all(), 51 | serializer=LUNSerializer, 52 | many=True 53 | ) 54 | 55 | class Meta: 56 | model = Datastore 57 | fields = ( 58 | 'id', 'url', 'display', 'name', 'lun', 59 | 'description', 'tags', 'custom_fields', 'created', 'last_updated', 60 | ) 61 | brief_fields = ( 62 | 'id', 'url', 'display', 'name', 'lun', 63 | ) 64 | 65 | 66 | class StorageSessionSerializer(NetBoxModelSerializer): 67 | url = serializers.HyperlinkedIdentityField( 68 | view_name='plugins-api:netbox_storage-api:storagesession-detail' 69 | ) 70 | cluster = ClusterSerializer(nested=True) 71 | datastores = SerializedPKRelatedField( 72 | queryset=Datastore.objects.all(), 73 | serializer=DatastoreSerializer, 74 | many=True 75 | ) 76 | 77 | class Meta: 78 | model = StorageSession 79 | fields = ( 80 | 'id', 'url', 'display', 'name', 'cluster', 'datastores', 81 | 'description', 'tags', 'custom_fields', 'created', 'last_updated', 82 | ) 83 | brief_fields = ( 84 | 'id', 'url', 'display', 'name', 'cluster', 'datastores', 85 | ) 86 | 87 | 88 | class VMDKSerializer(NetBoxModelSerializer): 89 | url = serializers.HyperlinkedIdentityField( 90 | view_name='plugins-api:netbox_storage-api:vmdk-detail' 91 | ) 92 | datastore = DatastoreSerializer(nested=True) 93 | vm = VirtualMachineSerializer(nested=True) 94 | 95 | class Meta: 96 | model = VMDK 97 | fields = ( 98 | 'id', 'url', 'display', 'vm', 'name', 'datastore', 99 | 'size', 'tags', 'custom_fields', 'created', 'last_updated', 100 | ) 101 | brief_fields = ( 102 | 'id', 'url', 'display', 'vm', 'name', 'datastore', 'size', 103 | ) 104 | -------------------------------------------------------------------------------- /netbox_storage/api/urls.py: -------------------------------------------------------------------------------- 1 | from netbox.api.routers import NetBoxRouter 2 | from . import views 3 | 4 | 5 | app_name = 'netbox_storage' 6 | 7 | router = NetBoxRouter() 8 | router.register('storagepool', views.StoragePoolViewSet) 9 | router.register('lun', views.LUNViewSet) 10 | router.register('datastore', views.DatastoreViewSet) 11 | router.register('storagesession', views.StorageSessionViewSet) 12 | router.register('vmdk', views.VMDKViewSet) 13 | 14 | urlpatterns = router.urls 15 | -------------------------------------------------------------------------------- /netbox_storage/api/views.py: -------------------------------------------------------------------------------- 1 | from netbox.api.viewsets import NetBoxModelViewSet 2 | 3 | from .. import filtersets, models 4 | from .serializers import StoragePoolSerializer, LUNSerializer, StorageSessionSerializer, DatastoreSerializer, VMDKSerializer 5 | 6 | 7 | class StoragePoolViewSet(NetBoxModelViewSet): 8 | queryset = models.StoragePool.objects.prefetch_related('device', 'tags') 9 | serializer_class = StoragePoolSerializer 10 | filterset_class = filtersets.StoragePoolFilterSet 11 | 12 | 13 | class LUNViewSet(NetBoxModelViewSet): 14 | queryset = models.LUN.objects.prefetch_related( 15 | 'storage_pool', 'tags' 16 | ) 17 | serializer_class = LUNSerializer 18 | filterset_class = filtersets.LUNFilterSet 19 | 20 | 21 | class DatastoreViewSet(NetBoxModelViewSet): 22 | queryset = models.Datastore.objects.prefetch_related( 23 | 'lun', 'tags' 24 | ) 25 | serializer_class = DatastoreSerializer 26 | filterset_class = filtersets.DatastoreFilterSet 27 | 28 | 29 | class StorageSessionViewSet(NetBoxModelViewSet): 30 | queryset = models.StorageSession.objects.prefetch_related( 31 | 'cluster', 'datastores', 'tags' 32 | ) 33 | serializer_class = StorageSessionSerializer 34 | filterset_class = filtersets.StorageSessionFilterSet 35 | 36 | 37 | class VMDKViewSet(NetBoxModelViewSet): 38 | queryset = models.VMDK.objects.prefetch_related( 39 | 'datastore', 'vm', 'tags' 40 | ) 41 | serializer_class = VMDKSerializer 42 | filterset_class = filtersets.VMDKFilterSet 43 | -------------------------------------------------------------------------------- /netbox_storage/filtersets.py: -------------------------------------------------------------------------------- 1 | from netbox.filtersets import NetBoxModelFilterSet 2 | import django_filters 3 | from virtualization.models import VirtualMachine 4 | from .models import StoragePool, LUN, StorageSession, Datastore, VMDK 5 | 6 | 7 | class StoragePoolFilterSet(NetBoxModelFilterSet): 8 | 9 | class Meta: 10 | model = StoragePool 11 | fields = ('id', 'name', 'device') 12 | 13 | def search(self, queryset, name, value): 14 | return queryset.filter(name__icontains=value) 15 | 16 | 17 | class LUNFilterSet(NetBoxModelFilterSet): 18 | 19 | class Meta: 20 | model = LUN 21 | fields = ('id', 'storage_pool', 'name', 'wwn',) 22 | 23 | def search(self, queryset, name, value): 24 | return queryset.filter(name__icontains=value) 25 | 26 | 27 | class DatastoreFilterSet(NetBoxModelFilterSet): 28 | reachable_by_vm = django_filters.ModelMultipleChoiceFilter( 29 | field_name='storage_sessions__cluster__virtual_machines', 30 | queryset=VirtualMachine.objects.all(), 31 | label='Reachable by these Virtual Machines' 32 | ) 33 | 34 | class Meta: 35 | model = Datastore 36 | fields = ('id', 'lun', 'name', 'reachable_by_vm',) 37 | 38 | def search(self, queryset, name, value): 39 | return queryset.filter(name__icontains=value) 40 | 41 | 42 | class StorageSessionFilterSet(NetBoxModelFilterSet): 43 | 44 | class Meta: 45 | model = StorageSession 46 | fields = ( 47 | 'id', 'name', 'cluster', 'datastores', 48 | ) 49 | 50 | def search(self, queryset, name, value): 51 | return queryset.filter(name__icontains=value) 52 | 53 | 54 | class VMDKFilterSet(NetBoxModelFilterSet): 55 | 56 | class Meta: 57 | model = VMDK 58 | fields = ( 59 | 'id', 'name', 'vm', 'datastore', 60 | ) 61 | 62 | def search(self, queryset, name, value): 63 | return queryset.filter(name__icontains=value) 64 | -------------------------------------------------------------------------------- /netbox_storage/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm, NetBoxModelImportForm 3 | from utilities.forms.fields import DynamicModelChoiceField, CSVModelChoiceField, DynamicModelMultipleChoiceField, CSVModelMultipleChoiceField 4 | from dcim.models import Device 5 | from virtualization.models import Cluster, VirtualMachine 6 | from .models import StoragePool, StorageSession, LUN, Datastore, VMDK 7 | 8 | 9 | # 10 | # Regular forms 11 | # 12 | 13 | class StoragePoolForm(NetBoxModelForm): 14 | device = DynamicModelChoiceField( 15 | queryset=Device.objects.all() 16 | ) 17 | 18 | class Meta: 19 | model = StoragePool 20 | fields = ('name', 'size', 'device', 'description', 'tags') 21 | 22 | 23 | class LUNForm(NetBoxModelForm): 24 | storage_pool = DynamicModelChoiceField( 25 | queryset=StoragePool.objects.all() 26 | ) 27 | 28 | class Meta: 29 | model = LUN 30 | fields = ('storage_pool', 'name', 'size', 'wwn', 'description') 31 | 32 | 33 | class DatastoreForm(NetBoxModelForm): 34 | lun = DynamicModelMultipleChoiceField( 35 | queryset=LUN.objects.all() 36 | ) 37 | 38 | class Meta: 39 | model = LUN 40 | fields = ('lun', 'name', 'description') 41 | 42 | 43 | class StorageSessionForm(NetBoxModelForm): 44 | cluster = DynamicModelChoiceField( 45 | queryset=Cluster.objects.all(), 46 | ) 47 | datastores = DynamicModelMultipleChoiceField( 48 | queryset=Datastore.objects.all() 49 | ) 50 | 51 | class Meta: 52 | model = StorageSession 53 | fields = ( 54 | 'name', 'cluster', 'datastores', 'description' 55 | ) 56 | 57 | 58 | class VMDKForm(NetBoxModelForm): 59 | vm = DynamicModelChoiceField( 60 | queryset=VirtualMachine.objects.all(), 61 | label='Virtual Machine' 62 | ) 63 | datastore = DynamicModelChoiceField( 64 | queryset=Datastore.objects.all(), 65 | query_params={ 66 | 'reachable_by_vm': '$vm' 67 | } 68 | ) 69 | 70 | class Meta: 71 | model = VMDK 72 | fields = ( 73 | 'vm', 'datastore', 'name', 'size', 74 | ) 75 | 76 | 77 | # 78 | # Filter forms 79 | # 80 | 81 | class StoragePoolFilterForm(NetBoxModelFilterSetForm): 82 | model = StoragePool 83 | device = DynamicModelMultipleChoiceField( 84 | queryset=Device.objects.all(), 85 | required=False 86 | ) 87 | name = forms.CharField( 88 | required=False 89 | ) 90 | 91 | 92 | class LUNFilterForm(NetBoxModelFilterSetForm): 93 | model = LUN 94 | storage_pool = DynamicModelMultipleChoiceField( 95 | queryset=StoragePool.objects.all(), 96 | required=False 97 | ) 98 | name = forms.CharField( 99 | required=False 100 | ) 101 | wwn = forms.CharField( 102 | required=False, 103 | label='WWN' 104 | ) 105 | 106 | 107 | class DatastoreFilterForm(NetBoxModelFilterSetForm): 108 | model = Datastore 109 | lun = DynamicModelMultipleChoiceField( 110 | queryset=LUN.objects.all(), 111 | required=False 112 | ) 113 | name = forms.CharField( 114 | required=False 115 | ) 116 | reachable_by_vm = DynamicModelMultipleChoiceField( 117 | queryset=VirtualMachine.objects.all(), 118 | required=False, 119 | label='Reachable by these Virtual Machines' 120 | ) 121 | 122 | 123 | class StorageSessionFilterForm(NetBoxModelFilterSetForm): 124 | model = StorageSession 125 | cluster = DynamicModelMultipleChoiceField( 126 | queryset=Cluster.objects.all(), 127 | required=False 128 | ) 129 | datastores = DynamicModelMultipleChoiceField( 130 | queryset=Datastore.objects.all(), 131 | required=False 132 | ) 133 | name = forms.CharField( 134 | required=False 135 | ) 136 | 137 | 138 | class VMDKFilterForm(NetBoxModelFilterSetForm): 139 | model = VMDK 140 | datastore = DynamicModelMultipleChoiceField( 141 | queryset=Datastore.objects.all(), 142 | required=False 143 | ) 144 | vm = DynamicModelMultipleChoiceField( 145 | queryset=VirtualMachine.objects.all(), 146 | required=False 147 | ) 148 | name = forms.CharField( 149 | required=False 150 | ) 151 | 152 | 153 | # 154 | # CSV Forms 155 | # 156 | 157 | class StoragePoolCSVForm(NetBoxModelImportForm): 158 | device = CSVModelChoiceField( 159 | queryset=Device.objects.all(), 160 | to_field_name='name', 161 | ) 162 | 163 | class Meta: 164 | model = StoragePool 165 | fields = ('name', 'size', 'device', 'description') 166 | 167 | 168 | class LUNCSVForm(NetBoxModelImportForm): 169 | storage_pool = CSVModelChoiceField( 170 | queryset=StoragePool.objects.all(), 171 | to_field_name='name', 172 | ) 173 | 174 | class Meta: 175 | model = LUN 176 | fields = ('storage_pool', 'name', 'size', 'wwn', 'description') 177 | 178 | 179 | class DatastoreCSVForm(NetBoxModelImportForm): 180 | lun = CSVModelMultipleChoiceField( 181 | queryset=LUN.objects.all(), 182 | to_field_name='name', 183 | help_text='A single LUN name or multiple LUN names separated by commas ("LUN" or "LUN1,LUN2,LUN3")' 184 | ) 185 | 186 | class Meta: 187 | model = Datastore 188 | fields = ('lun', 'name', 'description') 189 | 190 | 191 | class StorageSessionCSVForm(NetBoxModelImportForm): 192 | cluster = CSVModelChoiceField( 193 | queryset=Cluster.objects.all(), 194 | to_field_name='name', 195 | ) 196 | datastores = CSVModelMultipleChoiceField( 197 | queryset=Datastore.objects.all(), 198 | to_field_name='name', 199 | help_text='A single Datastore name or multiple Datastore names separated by commas ("datastore1" or "datastore1,datastore2,datastore3")' 200 | ) 201 | 202 | class Meta: 203 | model = StorageSession 204 | fields = ('cluster', 'datastores', 'name', 'description') 205 | 206 | 207 | class VMDKCSVForm(NetBoxModelImportForm): 208 | vm = CSVModelChoiceField( 209 | queryset=VirtualMachine.objects.all(), 210 | to_field_name='name', 211 | ) 212 | datastore = CSVModelChoiceField( 213 | queryset=Datastore.objects.all(), 214 | to_field_name='name', 215 | ) 216 | 217 | class Meta: 218 | model = VMDK 219 | fields = ('vm', 'datastore', 'name', 'size') 220 | -------------------------------------------------------------------------------- /netbox_storage/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-02-18 06:38 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import taggit.managers 6 | import utilities.json 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('extras', '0084_staging'), 15 | ('virtualization', '0034_standardize_description_comments'), 16 | ('dcim', '0167_module_status'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Datastore', 22 | fields=[ 23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 24 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 25 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 26 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 27 | ('name', models.CharField(max_length=100)), 28 | ('description', models.TextField(blank=True)), 29 | ], 30 | options={ 31 | 'ordering': ('name',), 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='VMDK', 36 | fields=[ 37 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 38 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 39 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 40 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 41 | ('name', models.CharField(max_length=100)), 42 | ('size', models.PositiveBigIntegerField()), 43 | ('datastore', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vmdks', to='netbox_storage.datastore')), 44 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 45 | ('vm', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='vmdks', to='virtualization.virtualmachine')), 46 | ], 47 | options={ 48 | 'verbose_name': 'VMDK', 49 | 'ordering': ('datastore', 'name'), 50 | }, 51 | ), 52 | migrations.CreateModel( 53 | name='StorageSession', 54 | fields=[ 55 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 56 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 57 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 58 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 59 | ('name', models.CharField(max_length=100)), 60 | ('description', models.TextField(blank=True)), 61 | ('cluster', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='storage_sessions', to='virtualization.cluster')), 62 | ('datastores', models.ManyToManyField(related_name='storage_sessions', to='netbox_storage.datastore')), 63 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 64 | ], 65 | options={ 66 | 'ordering': ('name',), 67 | }, 68 | ), 69 | migrations.CreateModel( 70 | name='StoragePool', 71 | fields=[ 72 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 73 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 74 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 75 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 76 | ('name', models.CharField(max_length=100)), 77 | ('size', models.PositiveBigIntegerField()), 78 | ('description', models.TextField(blank=True)), 79 | ('device', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.device')), 80 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 81 | ], 82 | options={ 83 | 'ordering': ('name',), 84 | }, 85 | ), 86 | migrations.CreateModel( 87 | name='LUN', 88 | fields=[ 89 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), 90 | ('created', models.DateTimeField(auto_now_add=True, null=True)), 91 | ('last_updated', models.DateTimeField(auto_now=True, null=True)), 92 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), 93 | ('name', models.CharField(max_length=100)), 94 | ('size', models.PositiveBigIntegerField()), 95 | ('description', models.TextField(blank=True)), 96 | ('wwn', models.CharField(blank=True, max_length=64)), 97 | ('storage_pool', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='luns', to='netbox_storage.storagepool')), 98 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), 99 | ], 100 | options={ 101 | 'ordering': ('name',), 102 | 'unique_together': {('storage_pool', 'name')}, 103 | }, 104 | ), 105 | migrations.AddField( 106 | model_name='datastore', 107 | name='lun', 108 | field=models.ManyToManyField(related_name='datastores', to='netbox_storage.lun'), 109 | ), 110 | migrations.AddField( 111 | model_name='datastore', 112 | name='tags', 113 | field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), 114 | ), 115 | ] 116 | -------------------------------------------------------------------------------- /netbox_storage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viroge/netbox-storage/2366cd9c717c234d17de3936bc2206265a769805/netbox_storage/migrations/__init__.py -------------------------------------------------------------------------------- /netbox_storage/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from django.db.models import Sum 4 | from netbox.models import NetBoxModel 5 | 6 | 7 | class StoragePool(NetBoxModel): 8 | name = models.CharField( 9 | max_length=100 10 | ) 11 | size = models.PositiveBigIntegerField( 12 | help_text='Size in bytes' 13 | ) 14 | device = models.ForeignKey( 15 | to='dcim.Device', 16 | on_delete=models.PROTECT 17 | ) 18 | description = models.TextField( 19 | blank=True 20 | ) 21 | 22 | class Meta: 23 | ordering = ('name',) 24 | 25 | def __str__(self): 26 | return self.name 27 | 28 | def get_absolute_url(self): 29 | return reverse('plugins:netbox_storage:storagepool', args=[self.pk]) 30 | 31 | def get_utilization(self): 32 | sum_alloc_size = self.luns.all().aggregate(Sum('size'))['size__sum'] 33 | if sum_alloc_size: 34 | utilization = float(sum_alloc_size) / self.size * 100 35 | else: 36 | utilization = 0 37 | 38 | return utilization 39 | 40 | 41 | class LUN(NetBoxModel): 42 | storage_pool = models.ForeignKey( 43 | to=StoragePool, 44 | on_delete=models.PROTECT, 45 | related_name='luns' 46 | ) 47 | name = models.CharField( 48 | max_length=100 49 | ) 50 | size = models.PositiveBigIntegerField( 51 | help_text='Size in bytes' 52 | ) 53 | description = models.TextField( 54 | blank=True 55 | ) 56 | wwn = models.CharField( 57 | max_length=64, 58 | blank=True, 59 | verbose_name='WWN' 60 | ) 61 | 62 | class Meta: 63 | ordering = ('name',) 64 | unique_together = ('storage_pool', 'name') 65 | 66 | def __str__(self): 67 | return f'{self.name}' 68 | 69 | def get_absolute_url(self): 70 | return reverse('plugins:netbox_storage:lun', args=[self.pk]) 71 | 72 | 73 | class Datastore(NetBoxModel): 74 | lun = models.ManyToManyField( 75 | to=LUN, 76 | related_name='datastores' 77 | ) 78 | name = models.CharField( 79 | max_length=100 80 | ) 81 | description = models.TextField( 82 | blank=True 83 | ) 84 | 85 | class Meta: 86 | ordering = ('name',) 87 | 88 | def __str__(self): 89 | return f'{self.name}' 90 | 91 | def get_absolute_url(self): 92 | return reverse('plugins:netbox_storage:datastore', args=[self.pk]) 93 | 94 | def get_utilization(self): 95 | sum_lun_size = self.lun.all().aggregate(Sum('size'))['size__sum'] 96 | sum_vmdk_size = self.vmdks.all().aggregate(Sum('size'))['size__sum'] 97 | if sum_lun_size and sum_vmdk_size: 98 | utilization = float(sum_vmdk_size) / float(sum_lun_size) * 100 99 | else: 100 | utilization = 0 101 | 102 | return utilization 103 | 104 | 105 | class StorageSession(NetBoxModel): 106 | name = models.CharField( 107 | max_length=100 108 | ) 109 | cluster = models.ForeignKey( 110 | to='virtualization.cluster', 111 | on_delete=models.PROTECT, 112 | related_name='storage_sessions' 113 | ) 114 | datastores = models.ManyToManyField( 115 | to=Datastore, 116 | related_name='storage_sessions' 117 | ) 118 | description = models.TextField( 119 | blank=True 120 | ) 121 | 122 | class Meta: 123 | ordering = ('name',) 124 | 125 | def __str__(self): 126 | return f'{self.name}' 127 | 128 | def get_absolute_url(self): 129 | return reverse('plugins:netbox_storage:storagesession', args=[self.pk]) 130 | 131 | 132 | class VMDK(NetBoxModel): 133 | vm = models.ForeignKey( 134 | to='virtualization.virtualmachine', 135 | on_delete=models.PROTECT, 136 | related_name='vmdks', 137 | verbose_name='Virtual Machine' 138 | ) 139 | name = models.CharField( 140 | max_length=100 141 | ) 142 | datastore = models.ForeignKey( 143 | to=Datastore, 144 | related_name='vmdks', 145 | on_delete=models.PROTECT 146 | ) 147 | size = models.PositiveBigIntegerField( 148 | help_text='Size in bytes' 149 | ) 150 | 151 | class Meta: 152 | ordering = ('datastore', 'name',) 153 | verbose_name = 'VMDK' 154 | 155 | def __str__(self): 156 | return f'{self.vm}-{self.datastore}-{self.name}' 157 | 158 | def get_absolute_url(self): 159 | return reverse('plugins:netbox_storage:vmdk', args=[self.pk]) 160 | -------------------------------------------------------------------------------- /netbox_storage/navigation.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginMenuItem, PluginMenu, PluginMenuButton 2 | 3 | storagepool_item = PluginMenuItem( 4 | link='plugins:netbox_storage:storagepool_list', 5 | link_text='Storage Pools', 6 | permissions=['netbox_storage.view_storagepool'], 7 | buttons=[ 8 | PluginMenuButton( 9 | link='plugins:netbox_storage:storagepool_add', 10 | title='Add', 11 | icon_class='mdi mdi-plus-thick', 12 | permissions=['netbox_storage.add_storagepool'], 13 | ), 14 | PluginMenuButton( 15 | link='plugins:netbox_storage:storagepool_import', 16 | title='Import', 17 | icon_class='mdi mdi-upload', 18 | permissions=['netbox_storage.add_storagepool'], 19 | ) 20 | ] 21 | ) 22 | 23 | lun_item = PluginMenuItem( 24 | link='plugins:netbox_storage:lun_list', 25 | link_text='LUNs', 26 | permissions=['netbox_storage.view_lun'], 27 | buttons=[ 28 | PluginMenuButton( 29 | link='plugins:netbox_storage:lun_add', 30 | title='Add', 31 | icon_class='mdi mdi-plus-thick', 32 | permissions=['netbox_storage.add_lun'], 33 | ), 34 | PluginMenuButton( 35 | link='plugins:netbox_storage:lun_import', 36 | title='Import', 37 | icon_class='mdi mdi-upload', 38 | permissions=['netbox_storage.add_lun'], 39 | ) 40 | ] 41 | ) 42 | 43 | datastore_item = PluginMenuItem( 44 | link='plugins:netbox_storage:datastore_list', 45 | link_text='Datastores', 46 | permissions=['netbox_storage.view_datastore'], 47 | buttons=[ 48 | PluginMenuButton( 49 | link='plugins:netbox_storage:datastore_add', 50 | title='Add', 51 | icon_class='mdi mdi-plus-thick', 52 | permissions=['netbox_storage.add_datastore'], 53 | ), 54 | PluginMenuButton( 55 | link='plugins:netbox_storage:datastore_import', 56 | title='Import', 57 | icon_class='mdi mdi-upload', 58 | permissions=['netbox_storage.add_datastore'], 59 | ) 60 | ] 61 | ) 62 | 63 | storagesession_item = PluginMenuItem( 64 | link='plugins:netbox_storage:storagesession_list', 65 | link_text='Storage Sessions', 66 | permissions=['netbox_storage.view_storagesession'], 67 | buttons=[ 68 | PluginMenuButton( 69 | link='plugins:netbox_storage:storagesession_add', 70 | title='Add', 71 | icon_class='mdi mdi-plus-thick', 72 | permissions=['netbox_storage.add_storagesession'], 73 | ), 74 | PluginMenuButton( 75 | link='plugins:netbox_storage:storagesession_import', 76 | title='Import', 77 | icon_class='mdi mdi-upload', 78 | permissions=['netbox_storage.add_storagesession'], 79 | ) 80 | ] 81 | ) 82 | 83 | vmdk_item = PluginMenuItem( 84 | link='plugins:netbox_storage:vmdk_list', 85 | link_text='VMDKs', 86 | permissions=['netbox_storage.view_vmdk'], 87 | buttons=[ 88 | PluginMenuButton( 89 | link='plugins:netbox_storage:vmdk_add', 90 | title='Add', 91 | icon_class='mdi mdi-plus-thick', 92 | permissions=['netbox_storage.add_vmdk'], 93 | ), 94 | PluginMenuButton( 95 | link='plugins:netbox_storage:vmdk_import', 96 | title='Import', 97 | icon_class='mdi mdi-upload', 98 | permissions=['netbox_storage.add_vmdk'], 99 | ) 100 | ] 101 | ) 102 | 103 | menu = PluginMenu( 104 | label='Storage', 105 | groups=( 106 | ('Storage', (storagepool_item, lun_item)), 107 | ('Virtualization', (datastore_item, storagesession_item, vmdk_item)) 108 | ), 109 | icon_class='mdi mdi-nas' 110 | ) 111 | -------------------------------------------------------------------------------- /netbox_storage/tables.py: -------------------------------------------------------------------------------- 1 | import django_tables2 as tables 2 | 3 | from django.template.defaultfilters import filesizeformat 4 | from netbox.tables import NetBoxTable, columns 5 | from .models import StoragePool, StorageSession, Datastore, LUN, VMDK 6 | 7 | 8 | class UtilizationColumn(columns.UtilizationColumn): 9 | template_code = """ 10 | {% load helpers %} 11 | {% if record.pk %} 12 | {% utilization_graph value %} 13 | {% endif %} 14 | """ 15 | 16 | 17 | class StoragePoolTable(NetBoxTable): 18 | name = tables.Column( 19 | linkify=True 20 | ) 21 | utilization = UtilizationColumn( 22 | accessor='get_utilization', 23 | orderable=False 24 | ) 25 | device = tables.Column( 26 | linkify=True 27 | ) 28 | 29 | class Meta(NetBoxTable.Meta): 30 | model = StoragePool 31 | fields = ( 32 | 'pk', 'id', 'name', 'size', 'utilization', 'device', 'description', 33 | 'actions' 34 | ) 35 | default_columns = ('name', 'device', 'size', 'utilization') 36 | 37 | def render_size(self, value): 38 | return filesizeformat(value) 39 | 40 | 41 | class LUNTable(NetBoxTable): 42 | name = tables.Column( 43 | linkify=True 44 | ) 45 | storage_pool = tables.Column( 46 | linkify=True 47 | ) 48 | 49 | class Meta(NetBoxTable.Meta): 50 | model = LUN 51 | fields = ( 52 | 'pk', 'id', 'name', 'storage_pool', 'size', 'wwn', 'description', 'actions', 53 | ) 54 | default_columns = ( 55 | 'name', 'storage_pool', 'size', 56 | ) 57 | 58 | def render_size(self, value): 59 | return filesizeformat(value) 60 | 61 | 62 | class DatastoreTable(NetBoxTable): 63 | name = tables.Column( 64 | linkify=True 65 | ) 66 | lun = columns.ManyToManyColumn( 67 | linkify_item=True, 68 | verbose_name='LUNs', 69 | ) 70 | utilization = UtilizationColumn( 71 | accessor='get_utilization', 72 | orderable=False 73 | ) 74 | 75 | class Meta(NetBoxTable.Meta): 76 | model = Datastore 77 | fields = ( 78 | 'pk', 'id', 'name', 'lun', 'utilization', 'description', 'actions', 79 | ) 80 | default_columns = ( 81 | 'name', 'lun', 'utilization', 82 | ) 83 | 84 | 85 | class StorageSessionTable(NetBoxTable): 86 | name = tables.Column( 87 | linkify=True 88 | ) 89 | cluster = tables.Column( 90 | linkify=True, 91 | ) 92 | datastores = columns.ManyToManyColumn( 93 | linkify_item=True, 94 | verbose_name='Datastores' 95 | ) 96 | 97 | class Meta(NetBoxTable.Meta): 98 | model = StorageSession 99 | 100 | fields = ( 101 | 'pk', 'id', 'name', 'cluster', 'datastores', 'description', 102 | ) 103 | default_columns = ( 104 | 'name', 'cluster', 'datastores', 105 | ) 106 | 107 | 108 | class VMDKTable(NetBoxTable): 109 | name = tables.Column( 110 | linkify=True 111 | ) 112 | vm = tables.Column( 113 | linkify=True 114 | ) 115 | datastore = tables.Column( 116 | linkify=True 117 | ) 118 | 119 | class Meta(NetBoxTable.Meta): 120 | model = VMDK 121 | 122 | fields = ( 123 | 'pk', 'id', 'vm', 'name', 'datastore', 'size', 124 | ) 125 | default_columns = ( 126 | 'vm', 'datastore', 'name', 'size', 127 | ) 128 | 129 | def render_size(self, value): 130 | return filesizeformat(value) 131 | -------------------------------------------------------------------------------- /netbox_storage/template_content.py: -------------------------------------------------------------------------------- 1 | from netbox.plugins import PluginTemplateExtension 2 | from .models import VMDK 3 | 4 | 5 | class VMVMDKCard(PluginTemplateExtension): 6 | models = ['virtualization.virtualmachine', ] 7 | 8 | def left_page(self): 9 | return self.render('netbox_storage/vm_vmdk_extend.html', 10 | extra_context={'object': self.context['object']}) 11 | 12 | 13 | template_extensions = [VMVMDKCard] 14 | -------------------------------------------------------------------------------- /netbox_storage/templates/netbox_storage/datastore.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load render_table from django_tables2 %} 3 | {% load helpers %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
Datastore
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Name{{ object.name }}
Utilization{% utilization_graph object.get_utilization %}
Description{{ object.description }}
24 |
25 | {% include 'inc/panels/custom_fields.html' %} 26 |
27 |
28 | {% include 'inc/panels/tags.html' %} 29 |
30 |
31 |
32 |
33 |
34 |
LUNs in this group
35 |
36 | {% render_table luns_table %} 37 |
38 |
39 |
40 |
Storage Sessions
41 |
42 | {% render_table sessions_table %} 43 |
44 |
45 |
46 |
VMDKs on this Datastore
47 |
48 | {% render_table vmdks_table %} 49 |
50 |
51 |
52 |
53 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_storage/templates/netbox_storage/lun.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load render_table from django_tables2 %} 3 | {% load helpers %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
LUN
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Name{{ object.name }}
Storage pool 18 | {{ object.storage_pool }} 19 |
Size{{ object.size|filesizeformat }}
WWN{{ object.wwn }}
Description{{ object.description }}
34 |
35 | {% include 'inc/panels/custom_fields.html' %} 36 |
37 |
38 | {% include 'inc/panels/tags.html' %} 39 |
40 |
41 |
42 |
43 |
44 |
LUNs
45 |
46 | {% render_table datastores_table %} 47 |
48 |
49 |
50 |
51 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_storage/templates/netbox_storage/storagepool.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | {% load render_table from django_tables2 %} 3 | {% load helpers %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
Storage Pool
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Name{{ object.name }}
Device 18 | {{ object.device }} 19 |
Size{{ object.size|filesizeformat }}
Utilization{% utilization_graph object.get_utilization %}
Description{{ object.description }}
34 |
35 | {% include 'inc/panels/custom_fields.html' %} 36 |
37 |
38 | {% include 'inc/panels/tags.html' %} 39 |
40 |
41 |
42 |
43 |
44 |
LUNs
45 |
46 | {% render_table luns_table %} 47 |
48 |
49 |
50 |
51 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_storage/templates/netbox_storage/storagesession.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
Storage Session
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 |
Name{{ object.name }}
Cluster{{ object.cluster }}
Datastores 20 | {% for datastore in object.datastores.all %} 21 | {{ datastore }} 22 | {% if not forloop.last %}, {% endif %} 23 | {% endfor %} 24 |
Description{{ object.description }}
31 |
32 | {% include 'inc/panels/custom_fields.html' %} 33 |
34 |
35 | {% include 'inc/panels/tags.html' %} 36 |
37 |
38 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_storage/templates/netbox_storage/vm_vmdk_extend.html: -------------------------------------------------------------------------------- 1 |
2 |
Attached VMDKs
3 |
4 |
5 | {% if object.vmdks.all %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for vmdk in object.vmdks.all %} 16 | 17 | 18 | 19 | 20 | 28 | 29 | {% endfor %} 30 |
DatastoreNameSize
{{ vmdk.datastore }}{{ vmdk.name }}{{ vmdk.size|filesizeformat }} 21 | 22 | 23 | 24 | 25 | 26 | 27 |
31 | {% else %} 32 | 33 | No VMDKs 34 | 35 | {% endif %} 36 | {% if perms.netbox_storage.add_vmdk %} 37 | 42 | {% endif %} 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /netbox_storage/templates/netbox_storage/vmdk.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/object.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
VMDK
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Virtal Machine{{ object.vm }}
Datastore{{ object.datastore }}
Name{{ object.name }}
Size{{ object.size|filesizeformat }}
26 |
27 | {% include 'inc/panels/custom_fields.html' %} 28 |
29 |
30 | {% include 'inc/panels/tags.html' %} 31 |
32 |
33 | {% endblock content %} -------------------------------------------------------------------------------- /netbox_storage/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import models, views 3 | from netbox.views.generic import ObjectChangeLogView 4 | 5 | 6 | urlpatterns = ( 7 | 8 | # Storage pools 9 | path('storagepool/', views.StoragePoolListView.as_view(), name='storagepool_list'), 10 | path('storagepool/add/', views.StoragePoolEditView.as_view(), name='storagepool_add'), 11 | path('storagepool/import/', views.StoragePoolImportView.as_view(), name='storagepool_import'), 12 | path('storagepool//', views.StoragePoolView.as_view(), name='storagepool'), 13 | path('storagepool//edit/', views.StoragePoolEditView.as_view(), name='storagepool_edit'), 14 | path('storagepool//delete/', views.StoragePoolDeleteView.as_view(), name='storagepool_delete'), 15 | path('storagepool/delete/', views.StoragePoolBulkDeleteView.as_view(), name='storagepool_bulk_delete'), 16 | path('storagepool//changelog/', ObjectChangeLogView.as_view(), name='storagepool_changelog', kwargs={ 17 | 'model': models.StoragePool 18 | }), 19 | 20 | # LUNs 21 | path('lun/', views.LUNListView.as_view(), name='lun_list'), 22 | path('lun/add/', views.LUNEditView.as_view(), name='lun_add'), 23 | path('lun/import/', views.LUNImportView.as_view(), name='lun_import'), 24 | path('lun//', views.LUNView.as_view(), name='lun'), 25 | path('lun//edit/', views.LUNEditView.as_view(), name='lun_edit'), 26 | path('lun//delete/', views.LUNDeleteView.as_view(), name='lun_delete'), 27 | path('lun/delete/', views.LUNBulkDeleteView.as_view(), name='lun_bulk_delete'), 28 | path('lun//changelog/', ObjectChangeLogView.as_view(), name='lun_changelog', kwargs={ 29 | 'model': models.LUN 30 | }), 31 | 32 | # Datastores 33 | path('datastore/', views.DatastoreListView.as_view(), name='datastore_list'), 34 | path('datastore/add/', views.DatastoreEditView.as_view(), name='datastore_add'), 35 | path('datastore/import/', views.DatastoreImportView.as_view(), name='datastore_import'), 36 | path('datastore//', views.DatastoreView.as_view(), name='datastore'), 37 | path('datastore//edit/', views.DatastoreEditView.as_view(), name='datastore_edit'), 38 | path('datastore//delete/', views.DatastoreDeleteView.as_view(), name='datastore_delete'), 39 | path('datastore/delete/', views.DatastoreBulkDeleteView.as_view(), name='datastore_bulk_delete'), 40 | path('datastore//changelog/', ObjectChangeLogView.as_view(), name='datastore_changelog', kwargs={ 41 | 'model': models.Datastore 42 | }), 43 | 44 | # Storage sessions 45 | path('storagesession/', views.StorageSessionListView.as_view(), name='storagesession_list'), 46 | path('storagesession/add/', views.StorageSessionEditView.as_view(), name='storagesession_add'), 47 | path('storagesession/import/', views.StorageSessionImportView.as_view(), name='storagesession_import'), 48 | path('storagesession//', views.StorageSessionView.as_view(), name='storagesession'), 49 | path('storagesession//edit/', views.StorageSessionEditView.as_view(), name='storagesession_edit'), 50 | path('storagesession//delete/', views.StorageSessionDeleteView.as_view(), name='storagesession_delete'), 51 | path('storagesession/delete/', views.StorageSessionBulkDeleteView.as_view(), name='storagesession_bulk_delete'), 52 | path('storagesession//changelog/', ObjectChangeLogView.as_view(), name='storagesession_changelog', kwargs={ 53 | 'model': models.StorageSession 54 | }), 55 | 56 | # VMDK 57 | path('vmdk/', views.VMDKListView.as_view(), name='vmdk_list'), 58 | path('vmdk/add/', views.VMDKEditView.as_view(), name='vmdk_add'), 59 | path('vmdk/import/', views.VMDKImportView.as_view(), name='vmdk_import'), 60 | path('vmdk//', views.VMDKView.as_view(), name='vmdk'), 61 | path('vmdk//edit/', views.VMDKEditView.as_view(), name='vmdk_edit'), 62 | path('vmdk//delete/', views.VMDKDeleteView.as_view(), name='vmdk_delete'), 63 | path('vmdk/delete/', views.VMDKBulkDeleteView.as_view(), name='vmdk_bulk_delete'), 64 | path('vmdk//changelog/', ObjectChangeLogView.as_view(), name='vmdk_changelog', kwargs={ 65 | 'model': models.VMDK 66 | }), 67 | ) 68 | -------------------------------------------------------------------------------- /netbox_storage/views.py: -------------------------------------------------------------------------------- 1 | from netbox.views import generic 2 | from . import filtersets, forms, models, tables 3 | 4 | 5 | # 6 | # StoragePool views 7 | # 8 | 9 | class StoragePoolView(generic.ObjectView): 10 | queryset = models.StoragePool.objects.all() 11 | 12 | def get_extra_context(self, request, instance): 13 | table = tables.LUNTable(instance.luns.all()) 14 | table.configure(request) 15 | 16 | return { 17 | 'luns_table': table, 18 | } 19 | 20 | 21 | class StoragePoolListView(generic.ObjectListView): 22 | queryset = models.StoragePool.objects.all() 23 | table = tables.StoragePoolTable 24 | filterset = filtersets.StoragePoolFilterSet 25 | filterset_form = forms.StoragePoolFilterForm 26 | 27 | 28 | class StoragePoolEditView(generic.ObjectEditView): 29 | queryset = models.StoragePool.objects.all() 30 | form = forms.StoragePoolForm 31 | 32 | 33 | class StoragePoolDeleteView(generic.ObjectDeleteView): 34 | queryset = models.StoragePool.objects.all() 35 | 36 | 37 | class StoragePoolBulkDeleteView(generic.BulkDeleteView): 38 | queryset = models.StoragePool.objects.all() 39 | table = tables.StoragePoolTable 40 | filterset = filtersets.StoragePoolFilterSet 41 | 42 | 43 | class StoragePoolImportView(generic.BulkImportView): 44 | queryset = models.StoragePool.objects.all() 45 | model_form = forms.StoragePoolCSVForm 46 | table = tables.StoragePoolTable 47 | 48 | 49 | # 50 | # LUN views 51 | # 52 | 53 | class LUNView(generic.ObjectView): 54 | queryset = models.LUN.objects.all() 55 | 56 | def get_extra_context(self, request, instance): 57 | datastores_table = tables.DatastoreTable(instance.datastores.all()) 58 | datastores_table.configure(request) 59 | 60 | return { 61 | 'datastores_table': datastores_table, 62 | } 63 | 64 | 65 | class LUNListView(generic.ObjectListView): 66 | queryset = models.LUN.objects.all() 67 | table = tables.LUNTable 68 | filterset = filtersets.LUNFilterSet 69 | filterset_form = forms.LUNFilterForm 70 | 71 | 72 | class LUNEditView(generic.ObjectEditView): 73 | queryset = models.LUN.objects.all() 74 | form = forms.LUNForm 75 | 76 | 77 | class LUNDeleteView(generic.ObjectDeleteView): 78 | queryset = models.LUN.objects.all() 79 | 80 | 81 | class LUNBulkDeleteView(generic.BulkDeleteView): 82 | queryset = models.LUN.objects.all() 83 | table = tables.LUNTable 84 | filterset = filtersets.LUNFilterSet 85 | 86 | 87 | class LUNImportView(generic.BulkImportView): 88 | queryset = models.LUN.objects.all() 89 | model_form = forms.LUNCSVForm 90 | table = tables.LUNTable 91 | 92 | 93 | # 94 | # StorageLUNGroup views 95 | # 96 | 97 | class DatastoreView(generic.ObjectView): 98 | queryset = models.Datastore.objects.all() 99 | 100 | def get_extra_context(self, request, instance): 101 | luns_table = tables.LUNTable(instance.lun.all()) 102 | luns_table.configure(request) 103 | 104 | sessions_table = tables.StorageSessionTable(instance.storage_sessions.all()) 105 | sessions_table.configure(request) 106 | 107 | vmdks_table = tables.VMDKTable(instance.vmdks.all()) 108 | vmdks_table.configure(request) 109 | 110 | return { 111 | 'luns_table': luns_table, 112 | 'sessions_table': sessions_table, 113 | 'vmdks_table': vmdks_table, 114 | } 115 | 116 | 117 | class DatastoreListView(generic.ObjectListView): 118 | queryset = models.Datastore.objects.all() 119 | table = tables.DatastoreTable 120 | filterset = filtersets.DatastoreFilterSet 121 | filterset_form = forms.DatastoreFilterForm 122 | 123 | 124 | class DatastoreEditView(generic.ObjectEditView): 125 | queryset = models.Datastore.objects.all() 126 | form = forms.DatastoreForm 127 | 128 | 129 | class DatastoreDeleteView(generic.ObjectDeleteView): 130 | queryset = models.Datastore.objects.all() 131 | 132 | 133 | class DatastoreBulkDeleteView(generic.BulkDeleteView): 134 | queryset = models.Datastore.objects.all() 135 | table = tables.DatastoreTable 136 | filterset = filtersets.DatastoreFilterSet 137 | 138 | 139 | class DatastoreImportView(generic.BulkImportView): 140 | queryset = models.Datastore.objects.all() 141 | model_form = forms.DatastoreCSVForm 142 | table = tables.DatastoreTable 143 | 144 | 145 | # 146 | # StorageSession views 147 | # 148 | 149 | class StorageSessionView(generic.ObjectView): 150 | queryset = models.StorageSession.objects.all() 151 | 152 | 153 | class StorageSessionListView(generic.ObjectListView): 154 | queryset = models.StorageSession.objects.all() 155 | table = tables.StorageSessionTable 156 | filterset = filtersets.StorageSessionFilterSet 157 | filterset_form = forms.StorageSessionFilterForm 158 | 159 | 160 | class StorageSessionEditView(generic.ObjectEditView): 161 | queryset = models.StorageSession.objects.all() 162 | form = forms.StorageSessionForm 163 | 164 | 165 | class StorageSessionDeleteView(generic.ObjectDeleteView): 166 | queryset = models.StorageSession.objects.all() 167 | 168 | 169 | class StorageSessionBulkDeleteView(generic.BulkDeleteView): 170 | queryset = models.StorageSession.objects.all() 171 | table = tables.StorageSessionTable 172 | filterset = filtersets.StorageSessionFilterSet 173 | 174 | 175 | class StorageSessionImportView(generic.BulkImportView): 176 | queryset = models.StorageSession.objects.all() 177 | model_form = forms.StorageSessionCSVForm 178 | table = tables.StorageSessionTable 179 | 180 | 181 | # 182 | # VMDK views 183 | # 184 | 185 | class VMDKView(generic.ObjectView): 186 | queryset = models.VMDK.objects.all() 187 | 188 | 189 | class VMDKListView(generic.ObjectListView): 190 | queryset = models.VMDK.objects.all() 191 | table = tables.VMDKTable 192 | filterset = filtersets.VMDKFilterSet 193 | filterset_form = forms.VMDKFilterForm 194 | 195 | 196 | class VMDKEditView(generic.ObjectEditView): 197 | queryset = models.VMDK.objects.all() 198 | form = forms.VMDKForm 199 | 200 | 201 | class VMDKDeleteView(generic.ObjectDeleteView): 202 | queryset = models.VMDK.objects.all() 203 | 204 | 205 | class VMDKBulkDeleteView(generic.BulkDeleteView): 206 | queryset = models.VMDK.objects.all() 207 | filterset = filtersets.VMDKFilterSet 208 | table = tables.VMDKTable 209 | 210 | 211 | class VMDKImportView(generic.BulkImportView): 212 | queryset = models.VMDK.objects.all() 213 | model_form = forms.VMDKCSVForm 214 | table = tables.VMDKTable 215 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open('README.md', 'r') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='netbox-storage-plugin', 8 | version='0.8.0', 9 | description='NetBox storage plugin', 10 | long_description=long_description, 11 | long_description_content_type='text/markdown', 12 | install_requires=[], 13 | packages=find_packages(), 14 | include_package_data=True, 15 | zip_safe=False, 16 | author='Gabor Somogyvari', 17 | url='https://github.com/viroge/netbox-storage', 18 | keywords=['netbox', 'netbox-plugin'], 19 | classifiers=[ 20 | 'Programming Language :: Python :: 3', 21 | 'License :: OSI Approved :: Apache Software License', 22 | 'Development Status :: 4 - Beta', 23 | ] 24 | ) 25 | --------------------------------------------------------------------------------