├── .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 |
10 |
11 |
12 | | Name |
13 | {{ object.name }} |
14 |
15 |
16 | | Utilization |
17 | {% utilization_graph object.get_utilization %} |
18 |
19 |
20 | | Description |
21 | {{ object.description }} |
22 |
23 |
24 |
25 | {% include 'inc/panels/custom_fields.html' %}
26 |
27 |
28 | {% include 'inc/panels/tags.html' %}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {% render_table luns_table %}
37 |
38 |
39 |
40 |
41 |
42 | {% render_table sessions_table %}
43 |
44 |
45 |
46 |
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 |
10 |
11 |
12 | | Name |
13 | {{ object.name }} |
14 |
15 |
16 | | Storage pool |
17 |
18 | {{ object.storage_pool }}
19 | |
20 |
21 |
22 | | Size |
23 | {{ object.size|filesizeformat }} |
24 |
25 |
26 | | WWN |
27 | {{ object.wwn }} |
28 |
29 |
30 | | Description |
31 | {{ object.description }} |
32 |
33 |
34 |
35 | {% include 'inc/panels/custom_fields.html' %}
36 |
37 |
38 | {% include 'inc/panels/tags.html' %}
39 |
40 |
41 |
42 |
43 |
44 |
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 |
10 |
11 |
12 | | Name |
13 | {{ object.name }} |
14 |
15 |
16 | | Device |
17 |
18 | {{ object.device }}
19 | |
20 |
21 |
22 | | Size |
23 | {{ object.size|filesizeformat }} |
24 |
25 |
26 | | Utilization |
27 | {% utilization_graph object.get_utilization %} |
28 |
29 |
30 | | Description |
31 | {{ object.description }} |
32 |
33 |
34 |
35 | {% include 'inc/panels/custom_fields.html' %}
36 |
37 |
38 | {% include 'inc/panels/tags.html' %}
39 |
40 |
41 |
42 |
43 |
44 |
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 |
8 |
9 |
10 | | Name |
11 | {{ object.name }} |
12 |
13 |
14 | | Cluster |
15 | {{ object.cluster }} |
16 |
17 |
18 | | Datastores |
19 |
20 | {% for datastore in object.datastores.all %}
21 | {{ datastore }}
22 | {% if not forloop.last %}, {% endif %}
23 | {% endfor %}
24 | |
25 |
26 |
27 | | Description |
28 | {{ object.description }} |
29 |
30 |
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 |
3 |
4 |
5 | {% if object.vmdks.all %}
6 |
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 |
8 |
9 |
10 | | Virtal Machine |
11 | {{ object.vm }} |
12 |
13 |
14 | | Datastore |
15 | {{ object.datastore }} |
16 |
17 |
18 | | Name |
19 | {{ object.name }} |
20 |
21 |
22 | | Size |
23 | {{ object.size|filesizeformat }} |
24 |
25 |
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 |
--------------------------------------------------------------------------------