114 | {% endblock %}
--------------------------------------------------------------------------------
/netbox_tunnels2/models.py:
--------------------------------------------------------------------------------
1 | """Tunnel Django model.
2 |
3 | (c) 2020 Justin Drew
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 | http://www.apache.org/licenses/LICENSE-2.0
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | """
14 |
15 | from django.db import models
16 | from django.urls import reverse
17 | from django.contrib.contenttypes.models import ContentType
18 | from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
19 |
20 | from netbox.models import NetBoxModel, OrganizationalModel
21 | from utilities.querysets import RestrictedQuerySet
22 | from tenancy.models import Tenant
23 | from .constants import TUNNEL_INTERFACE_ASSIGNMENT_MODELS
24 |
25 | from dcim.models import Device, Interface
26 |
27 |
28 |
29 |
30 | from utilities.choices import ChoiceSet
31 |
32 |
33 | class TunnelStatusChoices(ChoiceSet):
34 | CHOICES = [
35 | ('pending-config', 'Pending Configuration', 'orange'),
36 | ('configured', 'Configured', 'green'),
37 | ('pending-deletion', 'Pending Deletion', 'red'),
38 | ]
39 |
40 | class TunnelType(OrganizationalModel):
41 | name = models.CharField(
42 | max_length=100,
43 | unique=True
44 | )
45 | slug = models.SlugField(
46 | max_length=100,
47 | unique=True
48 | )
49 | objects = RestrictedQuerySet.as_manager()
50 |
51 |
52 | class Meta:
53 | ordering = ['name']
54 | def __str__(self):
55 | return self.name
56 |
57 | def get_absolute_url(self):
58 | return reverse('plugins:netbox_tunnels2:tunneltype', args=[self.pk])
59 |
60 |
61 | class PluginTunnel(NetBoxModel):
62 | """Tunnel model."""
63 | name = models.CharField(max_length=64)
64 | status = models.CharField(max_length=30, choices=TunnelStatusChoices, default='pending-config')
65 | tunnel_type = models.ForeignKey(
66 | to='TunnelType',
67 | on_delete=models.PROTECT,
68 | related_name='tunnels'
69 | )
70 | side_a_assigned_object_type = models.ForeignKey(
71 | to=ContentType,
72 | limit_choices_to=TUNNEL_INTERFACE_ASSIGNMENT_MODELS,
73 | on_delete=models.PROTECT,
74 | null=True,
75 | blank=True,
76 | related_name="side_a_assigned_object_type"
77 | )
78 | side_a_assigned_object_id = models.PositiveBigIntegerField(
79 | null=True,
80 | blank=True
81 | )
82 | side_a_assigned_object = GenericForeignKey(
83 | ct_field="side_a_assigned_object_type",
84 | fk_field="side_a_assigned_object_id"
85 | )
86 | side_b_assigned_object_type = models.ForeignKey(
87 | to=ContentType,
88 | limit_choices_to=TUNNEL_INTERFACE_ASSIGNMENT_MODELS,
89 | on_delete=models.PROTECT,
90 | null=True,
91 | blank=True,
92 | related_name="side_b_assigned_object_type"
93 | )
94 | side_b_assigned_object_id = models.PositiveBigIntegerField(
95 | null=True,
96 | blank=True
97 | )
98 | side_b_assigned_object = GenericForeignKey(
99 | ct_field="side_b_assigned_object_type",
100 | fk_field="side_b_assigned_object_id"
101 | )
102 | a_pub_address = models.ForeignKey(
103 | to='ipam.IPAddress',
104 | on_delete=models.PROTECT,
105 | related_name='tunnel_a_pub_address',
106 | verbose_name="Side A Public Address"
107 | )
108 | b_pub_address = models.ForeignKey(
109 | to='ipam.IPAddress',
110 | on_delete=models.PROTECT,
111 | related_name='tunnel_b_pub_address',
112 | verbose_name="Side B Public Address",
113 | null=True,
114 | blank=True
115 | )
116 | description = models.CharField(
117 | max_length=200,
118 | blank=True
119 | )
120 | tenant = models.ForeignKey(
121 | to=Tenant,
122 | on_delete=models.RESTRICT,
123 | null=True,
124 | blank=True
125 | )
126 | psk = models.CharField(verbose_name="Pre-shared Key", max_length=100, blank=True)
127 | comments = models.TextField(blank=True)
128 |
129 | class Meta:
130 | """Class to define what will be used to set order based on. Will be using the unique tunnel ID for this purpose."""
131 | verbose_name_plural='Tunnels'
132 | def __str__(self):
133 | """Class to define what identifies the Tunnel object. Will be using name for this."""
134 | return self.name
135 | def get_absolute_url(self):
136 | return reverse('plugins:netbox_tunnels2:tunnel', args=[self.pk])
137 | def get_status_color(self):
138 | return TunnelStatusChoices.colors.get(self.status)
139 |
140 |
141 | GenericRelation(
142 | to=PluginTunnel,
143 | content_type_field="side_a_assigned_object_type",
144 | object_id_field="side_a_assigned_object_id",
145 | related_query_name="interface",
146 | ).contribute_to_class(Interface, "tunnelassignments")
147 | GenericRelation(
148 | to=PluginTunnel,
149 | content_type_field="side_a_assigned_object_type",
150 | object_id_field="side_a_assigned_object_id",
151 | related_query_name="device",
152 | ).contribute_to_class(Device, "tunnels")
153 | GenericRelation(
154 | to=PluginTunnel,
155 | content_type_field="side_b_assigned_object_type",
156 | object_id_field="side_b_assigned_object_id",
157 | related_query_name="interface",
158 | ).contribute_to_class(Interface, "tunnelassignments_b")
159 | GenericRelation(
160 | to=PluginTunnel,
161 | content_type_field="side_b_assigned_object_type",
162 | object_id_field="side_b_assigned_object_id",
163 | related_query_name="device",
164 | ).contribute_to_class(Device, "tunnels_b")
--------------------------------------------------------------------------------
/netbox_tunnels2/api/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 | from django.core.exceptions import ObjectDoesNotExist
4 | from django.contrib.contenttypes.models import ContentType
5 | from ..constants import TUNNEL_INTERFACE_ASSIGNMENT_MODELS
6 | from netbox.api.fields import ContentTypeField
7 |
8 | from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
9 | from ..models import PluginTunnel, TunnelType
10 |
11 |
12 | from netbox.constants import NESTED_SERIALIZER_PREFIX
13 | from utilities.api import get_serializer_for_model
14 | from drf_spectacular.utils import extend_schema_field
15 |
16 | class NestedTunnelSerializer(WritableNestedSerializer):
17 | url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_tunnels2-api:tunnel-detail')
18 |
19 | class Meta:
20 | model = PluginTunnel
21 | fields = (
22 | 'id',
23 | 'url',
24 | 'display',
25 | 'name',
26 | )
27 |
28 | class TunnelSerializer(NetBoxModelSerializer):
29 | url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_tunnels2-api:tunnel-detail')
30 | side_a_assigned_object_type = ContentTypeField(
31 | queryset=ContentType.objects.filter(TUNNEL_INTERFACE_ASSIGNMENT_MODELS),
32 | allow_null=True
33 | )
34 | side_b_assigned_object_type = ContentTypeField(
35 | queryset=ContentType.objects.filter(TUNNEL_INTERFACE_ASSIGNMENT_MODELS),
36 | allow_null=True
37 | )
38 | side_a_assigned_object = serializers.SerializerMethodField(read_only=True)
39 | side_b_assigned_object = serializers.SerializerMethodField(read_only=True)
40 | class Meta:
41 | model = PluginTunnel
42 | fields = (
43 | 'id',
44 | 'url',
45 | 'display',
46 | 'name',
47 | 'tenant',
48 | 'a_pub_address',
49 | 'b_pub_address',
50 | 'side_a_assigned_object_type',
51 | 'side_a_assigned_object_id',
52 | 'side_a_assigned_object',
53 | 'side_b_assigned_object_type',
54 | 'side_b_assigned_object_id',
55 | 'side_b_assigned_object',
56 | 'status',
57 | 'tunnel_type',
58 | 'psk',
59 | 'tags',
60 | 'custom_fields',
61 | 'created',
62 | 'last_updated',
63 | )
64 | @extend_schema_field(serializers.DictField)
65 | def get_side_a_assigned_object(self, obj):
66 | if obj.side_a_assigned_object is None:
67 | return None
68 | serializer = get_serializer_for_model(
69 | obj.side_a_assigned_object,
70 | prefix=NESTED_SERIALIZER_PREFIX,
71 | )
72 | context = {"request": self.context["request"]}
73 | return serializer(obj.side_a_assigned_object, context=context).data
74 | def get_side_a_inner_ip(self, obj):
75 | if obj.side_a_assigned_object is None:
76 | return None
77 | serializer = get_serializer_for_model(
78 | obj.side_a_assigned_object,
79 | prefix=NESTED_SERIALIZER_PREFIX,
80 | )
81 | context = {"request": self.context["request"]}
82 | return serializer(obj.side_a_assigned_object, context=context).data
83 | def get_side_b_assigned_object(self, obj):
84 | if obj.side_b_assigned_object is None:
85 | return None
86 | serializer = get_serializer_for_model(
87 | obj.side_b_assigned_object,
88 | prefix=NESTED_SERIALIZER_PREFIX,
89 | )
90 | context = {"request": self.context["request"]}
91 | return serializer(obj.side_b_assigned_object, context=context).data
92 |
93 | def validate(self, data):
94 | """
95 | Validate the Tunnel django model's inputs before allowing it to update the instance.
96 | - Check that the GFK object is valid.
97 | - TODO: Check to see if the interface is assigned to another Tunnel
98 | """
99 | error_message = {}
100 |
101 | # Check that the GFK object is valid.
102 | if "side_a_assigned_object_type" in data and "side_a_assigned_object_id" in data:
103 | # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
104 | try:
105 | side_a_assigned_object = data[ # noqa: F841
106 | "side_a_assigned_object_type"
107 | ].get_object_for_this_type(
108 | id=data["side_a_assigned_object_id"],
109 | )
110 | except ObjectDoesNotExist:
111 | # Sets a standard error message for invalid GFK
112 | error_message_invalid_gfk = f"Invalid side_a_assigned_object {data['side_a_assigned_object_type']} ID {data['side_a_assigned_object_id']}"
113 | error_message["side_a_assigned_object_type"] = [error_message_invalid_gfk]
114 | error_message["side_a_assigned_object_id"] = [error_message_invalid_gfk]
115 | if "side_b_assigned_object_type" in data and "side_b_assigned_object_id" in data:
116 | # TODO: This can removed after https://github.com/netbox-community/netbox/issues/10221 is fixed.
117 | try:
118 | side_b_assigned_object = data[ # noqa: F841
119 | "side_a_assigned_object_type"
120 | ].get_object_for_this_type(
121 | id=data["side_b_assigned_object_id"],
122 | )
123 | except ObjectDoesNotExist:
124 | # Sets a standard error message for invalid GFK
125 | error_message_invalid_gfk = f"Invalid side_b_assigned_object {data['side_b_assigned_object_type']} ID {data['side_b_assigned_object_id']}"
126 | error_message["side_b_assigned_object_type"] = [error_message_invalid_gfk]
127 | error_message["side_b_assigned_object_id"] = [error_message_invalid_gfk]
128 |
129 | if data["side_a_assigned_object_type"].model == "interface":
130 | interface_host = (
131 | data["side_a_assigned_object_type"]
132 | .get_object_for_this_type(id=data["side_a_assigned_object_id"])
133 | .device
134 | )
135 | else:
136 | a_interface_host = None
137 | if data["side_b_assigned_object_type"].model == "interface":
138 | interface_host = (
139 | data["side_b_assigned_object_type"]
140 | .get_object_for_this_type(id=data["side_b_assigned_object_id"])
141 | .device
142 | )
143 | else:
144 | b_interface_host = None
145 |
146 | if error_message:
147 | raise serializers.ValidationError(error_message)
148 |
149 | return super().validate(data)
150 |
151 | #
152 | # Tunnel Type
153 | #
154 |
155 | class NestedTunnelTypeSerializer(WritableNestedSerializer):
156 | url = serializers.HyperlinkedIdentityField(view_name='plugins:netbox_tunnels2:tunneltype')
157 |
158 | class Meta:
159 | model = TunnelType
160 | fields = ('id', 'url', 'display', 'name')
161 |
162 | class TunnelTypeSerializer(NetBoxModelSerializer):
163 | url = serializers.HyperlinkedIdentityField(view_name='plugins:netbox_tunnels2:tunneltype')
164 |
165 | class Meta:
166 | model = TunnelType
167 | fields = ('id', 'url', 'display', 'name',)
--------------------------------------------------------------------------------
/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 [2023] [Robert Lynch]
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.
--------------------------------------------------------------------------------
/netbox_tunnels2/forms.py:
--------------------------------------------------------------------------------
1 | from django.forms import (
2 | CharField,
3 | ChoiceField,
4 | MultipleChoiceField,
5 | ChoiceField,
6 | PasswordInput,
7 | ModelChoiceField
8 | )
9 |
10 | from utilities.forms.fields import (
11 | DynamicModelChoiceField,
12 | SlugField,
13 | DynamicModelMultipleChoiceField
14 | )
15 |
16 | from dcim.models import Interface, Device
17 | from ipam.models import IPAddress, VRF
18 | from virtualization.models import VMInterface, VirtualMachine
19 | from ipam.formfields import IPNetworkFormField
20 | from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError
21 | from django.contrib.contenttypes.models import ContentType
22 |
23 | from netbox.forms import NetBoxModelForm, NetBoxModelFilterSetForm
24 | from tenancy.models import Tenant
25 |
26 | from .models import PluginTunnel, TunnelStatusChoices, TunnelType
27 |
28 |
29 | class TunnelEditForm(NetBoxModelForm):
30 | """Form for creating a new tunnel."""
31 |
32 | name = CharField(required=True, label="Name", help_text="Name of tunnel")
33 |
34 | status = ChoiceField(
35 | choices=TunnelStatusChoices,
36 | required=False
37 | )
38 |
39 | tunnel_type = ModelChoiceField(
40 | queryset=TunnelType.objects.all(),
41 | required=True
42 | )
43 | a_pub_VRF = DynamicModelChoiceField(
44 | label='Side A Address VRF',
45 | queryset=VRF.objects.all(),
46 | required=False,
47 | selector=True,
48 | query_params={
49 | "tenant_id": "$tenant",
50 | },
51 | )
52 | a_pub_address = DynamicModelChoiceField(
53 | label='Side A Public IP Address',
54 | queryset=IPAddress.objects.all(),
55 | query_params={
56 | 'vrf_id': '$a_pub_VRF',
57 | 'device_id': '$side_a_device',
58 | 'virtual_machine_id': '$side_a_virtual_machine'
59 | }
60 | )
61 | b_pub_VRF = DynamicModelChoiceField(
62 | label='Side B Address VRF',
63 | queryset=VRF.objects.all(),
64 | selector=True,
65 | required=False
66 | )
67 | b_pub_address = DynamicModelChoiceField(
68 | label='Side B Public IP Address',
69 | queryset=IPAddress.objects.all(),
70 | query_params={
71 | 'vrf_id': '$b_pub_VRF',
72 | 'device_id': '$side_b_device',
73 | 'virtual_machine_id': '$side_b_virtual_machine'
74 | },
75 | required=False
76 | )
77 | psk = CharField(required=False, label="Pre-shared Key", help_text="Pre-shared key")
78 |
79 | side_a_virtual_machine = DynamicModelChoiceField(
80 | queryset=VirtualMachine.objects.all(),
81 | label="Site A VM",
82 | required=False,
83 | selector=True,
84 | query_params={
85 | "tenant_id": "$tenant",
86 | },
87 | )
88 | side_a_device = DynamicModelChoiceField(
89 | queryset=Device.objects.all(),
90 | label="Site A Device",
91 | required=False,
92 | selector=True,
93 | query_params={
94 | "tenant_id": "$tenant",
95 | },
96 | )
97 | side_a_device_interface = DynamicModelChoiceField(
98 | queryset=Interface.objects.all(),
99 | label="Site A Interface",
100 | required=False,
101 | query_params={
102 | "device_id": "$side_a_device",
103 | },
104 | )
105 | side_a_vm_interface = DynamicModelChoiceField(
106 | queryset=VMInterface.objects.all(),
107 | label="Site A Interface",
108 | required=False,
109 | query_params={
110 | "virtual_machine_id": "$side_a_virtual_machine",
111 | },
112 | )
113 | side_b_virtual_machine = DynamicModelChoiceField(
114 | queryset=VirtualMachine.objects.all(),
115 | label="Site B VM",
116 | required=False,
117 | selector=True,
118 | query_params={
119 | "tenant_id": "$tenant",
120 | },
121 | )
122 | side_b_device = DynamicModelChoiceField(
123 | queryset=Device.objects.all(),
124 | label="Site B Device",
125 | required=False,
126 | selector=True,
127 | )
128 | side_b_device_interface = DynamicModelChoiceField(
129 | queryset=Interface.objects.all(),
130 | label="Site B Interface",
131 | required=False,
132 | query_params={
133 | "device_id": "$side_b_device",
134 | },
135 | )
136 | side_b_vm_interface = DynamicModelChoiceField(
137 | queryset=VMInterface.objects.all(),
138 | label="Site B Interface",
139 | required=False,
140 | query_params={
141 | "virtual_machine_id": "$side_b_virtual_machine",
142 | },
143 | )
144 |
145 | def __init__(self, *args, **kwargs):
146 |
147 | # Initialize helper selectors
148 | instance = kwargs.get("instance")
149 | initial = kwargs.get("initial", {}).copy()
150 | if instance:
151 | if type(instance.side_a_assigned_object) is Interface:
152 | initial["side_a_device_interface"] = instance.side_a_assigned_object
153 | initial["side_a_device"] = instance.side_a_assigned_object.device
154 | elif type(instance.side_a_assigned_object) is VMInterface:
155 | initial["side_a_vm_interface"] = instance.side_a_assigned_object
156 | initial["side_a_virtual_machine"] = instance.side_a_assigned_object.virtual_machine
157 | if type(instance.side_b_assigned_object) is Interface:
158 | initial["side_b_device_interface"] = instance.side_b_assigned_object
159 | initial["side_b_device"] = instance.side_b_assigned_object.device
160 | elif type(instance.side_b_assigned_object) is VMInterface:
161 | initial["side_b_vm_interface"] = instance.side_b_assigned_object
162 | initial["side_b_virtual_machine"] = instance.side_b_assigned_object.virtual_machine
163 | if hasattr(instance, 'a_pub_address') and type(instance.a_pub_address) is IPAddress:
164 | initial["a_pub_VRF"] = instance.a_pub_address.vrf
165 | if hasattr(instance, 'b_pub_address') and type(instance.b_pub_address) is IPAddress:
166 | initial["b_pub_VRF"] = instance.b_pub_address.vrf
167 | kwargs["initial"] = initial
168 |
169 | super().__init__(*args, **kwargs)
170 |
171 | class Meta:
172 | """Class to define what is used to create a new network tunnel."""
173 |
174 | model = PluginTunnel
175 | fields = (
176 | "name",
177 | "status",
178 | "tunnel_type",
179 | "a_pub_address",
180 | "b_pub_address",
181 | "psk",
182 | 'comments',
183 | 'tags',
184 | 'tenant',
185 | )
186 |
187 | def clean(self):
188 | super().clean()
189 | error_message = {}
190 | name = self.cleaned_data['name']
191 | status = self.cleaned_data['status']
192 | tunnel_type = self.cleaned_data['tunnel_type']
193 | a_pub_address = self.cleaned_data['a_pub_address']
194 | b_pub_address = self.cleaned_data['b_pub_address']
195 | psk = self.cleaned_data['psk']
196 | side_a_device_interface = self.cleaned_data['side_a_device_interface']
197 | side_a_vm_interface = self.cleaned_data['side_a_vm_interface']
198 | side_b_device_interface = self.cleaned_data['side_b_device_interface']
199 | side_b_vm_interface = self.cleaned_data['side_b_vm_interface']
200 |
201 | # Check that interface or vminterface are set
202 | # if either the Side A or Side B interfaces are assigned
203 | if side_a_device_interface:
204 | side_a_assigned_object = side_a_device_interface
205 | side_a_assigned_object_type = "interface"
206 | side_a_host_type = "device"
207 | side_a_host = Interface.objects.get(pk=side_a_assigned_object.pk).device
208 | side_a_assigned_object_id = Interface.objects.get(pk=side_a_assigned_object.pk).pk
209 | if side_a_vm_interface:
210 | side_a_assigned_object = side_a_vm_interface
211 | side_a_assigned_object_type = "interfaces"
212 | side_a_host_type = "virtual_machine"
213 | side_a_host = VMInterface.objects.get(pk=side_a_assigned_object.pk).virtual_machine
214 | side_a_assigned_object_id = VMInterface.objects.get(pk=side_a_assigned_object.pk).pk
215 | if side_a_device_interface or side_a_vm_interface:
216 | side_a_assigned_object_type_id = ContentType.objects.get_for_model(side_a_assigned_object, ).pk
217 | if side_b_device_interface:
218 | side_b_assigned_object = side_b_device_interface
219 | side_b_assigned_object_type = "interface"
220 | side_b_host_type = "device"
221 | side_b_host = Interface.objects.get(pk=side_b_assigned_object.pk).device
222 | side_b_assigned_object_id = Interface.objects.get(pk=side_b_assigned_object.pk).pk
223 | if side_b_vm_interface:
224 | side_b_assigned_object = side_b_vm_interface
225 | side_b_assigned_object_type = "interfaces"
226 | side_b_host_type = "virtual_machine"
227 | side_b_host = VMInterface.objects.get(pk=side_b_assigned_object.pk).virtual_machine
228 | side_b_assigned_object_id = VMInterface.objects.get(pk=side_b_assigned_object.pk).pk
229 | if side_b_device_interface or side_b_vm_interface:
230 | side_b_assigned_object_type_id = ContentType.objects.get_for_model(side_b_assigned_object, ).pk
231 |
232 | def save(self, *args, **kwargs):
233 | # Set assigned object
234 | self.instance.side_a_assigned_object = (
235 | self.cleaned_data.get("side_a_device_interface")
236 | or self.cleaned_data.get("side_a_vm_interface")
237 | )
238 | self.instance.side_b_assigned_object = (
239 | self.cleaned_data.get("side_b_device_interface")
240 | or self.cleaned_data.get("side_b_vm_interface")
241 | )
242 | return super().save(*args, **kwargs)
243 |
244 |
245 | class TunnelAddForm(TunnelEditForm):
246 | tunnel_type = ModelChoiceField(
247 | queryset=TunnelType.objects.all(),
248 | required=True
249 | )
250 | remote_VRF = DynamicModelChoiceField(label='Remote Address VRF', queryset=VRF.objects.all(), required=False)
251 | b_pub_address = IPNetworkFormField(required=True)
252 |
253 | fields = (
254 | "name",
255 | "status",
256 | "tunnel_type",
257 | "a_pub_VRF",
258 | "a_pub_address",
259 | "b_pub_VRF",
260 | "b_pub_address",
261 | "psk",
262 | "comments",
263 | "tags",
264 | "tenant",
265 | )
266 | field_order = ["name",
267 | "status",
268 | "tunnel_type",
269 | "a_pub_VRF",
270 | "a_pub_address",
271 | "b_pub_VRF",
272 | "b_pub_address",
273 | "psk",
274 | "tenant",
275 | "comments",
276 | "tags"]
277 |
278 | def clean_b_pub_address(self):
279 |
280 | if self.data['remote_VRF'] != '':
281 | vrf = VRF.objects.get(id=self.data['remote_VRF'])
282 | else:
283 | vrf = 0
284 | try:
285 | if vrf == 0:
286 | ip = IPAddress.objects.get(address=str(self.cleaned_data['b_pub_address']))
287 | else:
288 | ip = IPAddress.objects.get(address=str(self.cleaned_data['b_pub_address']), vrf=vrf)
289 | except MultipleObjectsReturned:
290 | if vrf == 0:
291 | ip = IPAddress.objects.filter(address=str(self.cleaned_data['b_pub_address'])).first()
292 | else:
293 | ip = IPAddress.objects.filter(address=str(self.cleaned_data['b_pub_address']), vrf=vrf).first()
294 | except ObjectDoesNotExist:
295 | if vrf == 0:
296 | ip = IPAddress.objects.create(address=str(self.cleaned_data['b_pub_address']))
297 | else:
298 | ip = IPAddress.objects.create(address=str(self.cleaned_data['b_pub_address']), vrf=vrf)
299 | self.cleaned_data['b_pub_address'] = ip
300 | return self.cleaned_data['b_pub_address']
301 |
302 |
303 | class TunnelFilterForm(NetBoxModelFilterSetForm):
304 | """Form for filtering Tunnel instances."""
305 | model = PluginTunnel
306 | status = MultipleChoiceField(choices=TunnelStatusChoices, required=False)
307 | tenant_id = DynamicModelMultipleChoiceField(
308 | required=False, queryset=Tenant.objects.all(), label="Tenant"
309 | )
310 | tunnel_type = ModelChoiceField(
311 | queryset=TunnelType.objects.all(),
312 | required=False
313 | )
314 | a_pub_address = DynamicModelMultipleChoiceField(
315 | queryset=IPAddress.objects.all(),
316 | required=False,
317 | label="Local Address",
318 | )
319 | b_pub_address = DynamicModelMultipleChoiceField(
320 | queryset=IPAddress.objects.all(),
321 | required=False,
322 | label="Remote Address",
323 | )
324 |
325 | class Meta:
326 | """Class to define what is used for filtering tunnels with the search box."""
327 | model = PluginTunnel
328 | fields = (
329 | "a_pub_address",
330 | "b_pub_address",
331 | "psk",
332 | "tunnel_type",
333 | "tenant_id",
334 | )
335 |
336 |
337 | #
338 | # Tunnel Type
339 | #
340 | class TunnelTypeEditForm(NetBoxModelForm):
341 | """Form for creating a new tunnel."""
342 | slug = SlugField()
343 |
344 | class Meta:
345 | """Class to define what is used to create a new network tunnel."""
346 | model = TunnelType
347 | fields = ('name', 'slug')
348 |
--------------------------------------------------------------------------------
/.devcontainer/configuration/configuration.py:
--------------------------------------------------------------------------------
1 | # Based on https://github.com/netbox-community/netbox-docker/blob/release/configuration/configuration.py
2 | import re
3 | from os import environ
4 | from os.path import abspath, dirname, join
5 |
6 |
7 | # Read secret from file
8 | def _read_secret(secret_name, default=None):
9 | try:
10 | f = open("/run/secrets/" + secret_name, encoding="utf-8")
11 | except OSError:
12 | return default
13 | else:
14 | with f:
15 | return f.readline().strip()
16 |
17 |
18 | _BASE_DIR = dirname(dirname(abspath(__file__)))
19 |
20 | #########################
21 | # #
22 | # Required settings #
23 | # #
24 | #########################
25 |
26 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write
27 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name.
28 | #
29 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
30 | ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ")
31 |
32 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
33 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases
34 | DATABASE = {
35 | "NAME": environ.get("DB_NAME", "netbox"), # Database name
36 | "USER": environ.get("DB_USER", ""), # PostgreSQL username
37 | "PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")),
38 | # PostgreSQL password
39 | "HOST": environ.get("DB_HOST", "localhost"), # Database server
40 | "PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default)
41 | "OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")},
42 | # Database connection SSLMODE
43 | "CONN_MAX_AGE": int(environ.get("DB_CONN_MAX_AGE", "300")),
44 | # Max database connection age
45 | "DISABLE_SERVER_SIDE_CURSORS": environ.get(
46 | "DB_DISABLE_SERVER_SIDE_CURSORS",
47 | "False",
48 | ).lower()
49 | == "true",
50 | # Disable the use of server-side cursors transaction pooling
51 | }
52 |
53 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
54 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
55 | # to use two separate database IDs.
56 | REDIS = {
57 | "tasks": {
58 | "HOST": environ.get("REDIS_HOST", "localhost"),
59 | "PORT": int(environ.get("REDIS_PORT", 6379)),
60 | "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")),
61 | "DATABASE": int(environ.get("REDIS_DATABASE", 0)),
62 | "SSL": environ.get("REDIS_SSL", "False").lower() == "true",
63 | "INSECURE_SKIP_TLS_VERIFY": environ.get(
64 | "REDIS_INSECURE_SKIP_TLS_VERIFY",
65 | "False",
66 | ).lower()
67 | == "true",
68 | },
69 | "caching": {
70 | "HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")),
71 | "PORT": int(environ.get("REDIS_CACHE_PORT", environ.get("REDIS_PORT", 6379))),
72 | "PASSWORD": _read_secret(
73 | "redis_cache_password",
74 | environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")),
75 | ),
76 | "DATABASE": int(environ.get("REDIS_CACHE_DATABASE", 1)),
77 | "SSL": environ.get("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False")).lower()
78 | == "true",
79 | "INSECURE_SKIP_TLS_VERIFY": environ.get(
80 | "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY",
81 | environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"),
82 | ).lower()
83 | == "true",
84 | },
85 | }
86 |
87 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.
88 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and
89 | # symbols. NetBox will not run without this defined. For more information, see
90 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY
91 | SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", ""))
92 |
93 |
94 | #########################
95 | # #
96 | # Optional settings #
97 | # #
98 | #########################
99 |
100 | # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of
101 | # application errors (assuming correct email settings are provided).
102 | ADMINS = [
103 | # ['John Doe', 'jdoe@example.com'],
104 | ]
105 |
106 | # URL schemes that are allowed within links in NetBox
107 | ALLOWED_URL_SCHEMES = (
108 | "file",
109 | "ftp",
110 | "ftps",
111 | "http",
112 | "https",
113 | "irc",
114 | "mailto",
115 | "sftp",
116 | "ssh",
117 | "tel",
118 | "telnet",
119 | "tftp",
120 | "vnc",
121 | "xmpp",
122 | )
123 |
124 | # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same
125 | # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
126 | BANNER_TOP = environ.get("BANNER_TOP", "")
127 | BANNER_BOTTOM = environ.get("BANNER_BOTTOM", "")
128 |
129 | # Text to include on the login page above the login form. HTML is allowed.
130 | BANNER_LOGIN = environ.get("BANNER_LOGIN", "")
131 |
132 | # Base URL path if accessing NetBox within a directory. For example, if installed at http://example.com/netbox/, set:
133 | # BASE_PATH = 'netbox/'
134 | BASE_PATH = environ.get("BASE_PATH", "")
135 |
136 | # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
137 | CHANGELOG_RETENTION = int(environ.get("CHANGELOG_RETENTION", 90))
138 |
139 | # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be
140 | # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or
141 | # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers
142 | CORS_ORIGIN_ALLOW_ALL = environ.get("CORS_ORIGIN_ALLOW_ALL", "False").lower() == "true"
143 | CORS_ORIGIN_WHITELIST = list(
144 | filter(None, environ.get("CORS_ORIGIN_WHITELIST", "https://localhost").split(" ")),
145 | )
146 | CORS_ORIGIN_REGEX_WHITELIST = [
147 | re.compile(r)
148 | for r in list(
149 | filter(None, environ.get("CORS_ORIGIN_REGEX_WHITELIST", "").split(" ")),
150 | )
151 | ]
152 |
153 | # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
154 | # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
155 | # on a production system.
156 | DEBUG = environ.get("DEBUG", "False").lower() == "true"
157 |
158 | # Set to True to enable DEVELOPER Mode. WARNING: ONLY netbox developers or plugin developers need this access.
159 | DEVELOPER = environ.get("DEVELOPER_MODE", "False").lower() == "true"
160 |
161 | # Email settings
162 | EMAIL = {
163 | "SERVER": environ.get("EMAIL_SERVER", "localhost"),
164 | "PORT": int(environ.get("EMAIL_PORT", 25)),
165 | "USERNAME": environ.get("EMAIL_USERNAME", ""),
166 | "PASSWORD": _read_secret("email_password", environ.get("EMAIL_PASSWORD", "")),
167 | "USE_SSL": environ.get("EMAIL_USE_SSL", "False").lower() == "true",
168 | "USE_TLS": environ.get("EMAIL_USE_TLS", "False").lower() == "true",
169 | "SSL_CERTFILE": environ.get("EMAIL_SSL_CERTFILE", ""),
170 | "SSL_KEYFILE": environ.get("EMAIL_SSL_KEYFILE", ""),
171 | "TIMEOUT": int(environ.get("EMAIL_TIMEOUT", 10)), # seconds
172 | "FROM_EMAIL": environ.get("EMAIL_FROM", ""),
173 | }
174 |
175 | # Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table
176 | # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
177 | ENFORCE_GLOBAL_UNIQUE = environ.get("ENFORCE_GLOBAL_UNIQUE", "False").lower() == "true"
178 |
179 | # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and
180 | # by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models.
181 | EXEMPT_VIEW_PERMISSIONS = list(
182 | filter(None, environ.get("EXEMPT_VIEW_PERMISSIONS", "").split(" ")),
183 | )
184 |
185 | # Enable GraphQL API.
186 | GRAPHQL_ENABLED = environ.get("GRAPHQL_ENABLED", "True").lower() == "true"
187 |
188 | # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
189 | # https://docs.djangoproject.com/en/stable/topics/logging/
190 | LOGGING = {}
191 |
192 | # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
193 | # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
194 | LOGIN_REQUIRED = environ.get("LOGIN_REQUIRED", "False").lower() == "true"
195 |
196 | # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to
197 | # re-authenticate. (Default: 1209600 [14 days])
198 | LOGIN_TIMEOUT = int(environ.get("LOGIN_TIMEOUT", 1209600))
199 |
200 | # Setting this to True will display a "maintenance mode" banner at the top of every page.
201 | MAINTENANCE_MODE = environ.get("MAINTENANCE_MODE", "False").lower() == "true"
202 |
203 | # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
204 | # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
205 | # all objects by specifying "?limit=0".
206 | MAX_PAGE_SIZE = int(environ.get("MAX_PAGE_SIZE", 1000))
207 |
208 | # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that
209 | # the default value of this setting is derived from the installed location.
210 | MEDIA_ROOT = environ.get("MEDIA_ROOT", join(_BASE_DIR, "media"))
211 |
212 | # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics'
213 | METRICS_ENABLED = environ.get("METRICS_ENABLED", "False").lower() == "true"
214 |
215 | # Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
216 | NAPALM_USERNAME = environ.get("NAPALM_USERNAME", "")
217 | NAPALM_PASSWORD = _read_secret("napalm_password", environ.get("NAPALM_PASSWORD", ""))
218 |
219 | # NAPALM timeout (in seconds). (Default: 30)
220 | NAPALM_TIMEOUT = int(environ.get("NAPALM_TIMEOUT", 30))
221 |
222 | # NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
223 | # be provided as a dictionary.
224 | NAPALM_ARGS = {}
225 |
226 | # Determine how many objects to display per page within a list. (Default: 50)
227 | PAGINATE_COUNT = int(environ.get("PAGINATE_COUNT", 50))
228 |
229 | # Enable installed plugins. Add the name of each plugin to the list.
230 | PLUGINS = []
231 |
232 | # Plugins configuration settings. These settings are used by various plugins that the user may have installed.
233 | # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings.
234 | PLUGINS_CONFIG = {}
235 |
236 | # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to
237 | # prefer IPv4 instead.
238 | PREFER_IPV4 = environ.get("PREFER_IPV4", "False").lower() == "true"
239 |
240 | # Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1.
241 | RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = int(
242 | environ.get("RACK_ELEVATION_DEFAULT_UNIT_HEIGHT", 22),
243 | )
244 | RACK_ELEVATION_DEFAULT_UNIT_WIDTH = int(
245 | environ.get("RACK_ELEVATION_DEFAULT_UNIT_WIDTH", 220),
246 | )
247 |
248 | # Remote authentication support
249 | REMOTE_AUTH_ENABLED = environ.get("REMOTE_AUTH_ENABLED", "False").lower() == "true"
250 | REMOTE_AUTH_BACKEND = environ.get(
251 | "REMOTE_AUTH_BACKEND",
252 | "netbox.authentication.RemoteUserBackend",
253 | )
254 | REMOTE_AUTH_HEADER = environ.get("REMOTE_AUTH_HEADER", "HTTP_REMOTE_USER")
255 | REMOTE_AUTH_AUTO_CREATE_USER = (
256 | environ.get("REMOTE_AUTH_AUTO_CREATE_USER", "True").lower() == "true"
257 | )
258 | REMOTE_AUTH_DEFAULT_GROUPS = list(
259 | filter(None, environ.get("REMOTE_AUTH_DEFAULT_GROUPS", "").split(" ")),
260 | )
261 |
262 | # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the
263 | # version check or use the URL below to check for release in the official NetBox repository.
264 | # https://api.github.com/repos/netbox-community/netbox/releases
265 | RELEASE_CHECK_URL = environ.get("RELEASE_CHECK_URL", None)
266 |
267 | # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
268 | # this setting is derived from the installed location.
269 | REPORTS_ROOT = environ.get("REPORTS_ROOT", "/etc/netbox/reports")
270 |
271 | # Maximum execution time for background tasks, in seconds.
272 | RQ_DEFAULT_TIMEOUT = int(environ.get("RQ_DEFAULT_TIMEOUT", 300))
273 |
274 | # The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
275 | # this setting is derived from the installed location.
276 | SCRIPTS_ROOT = environ.get("SCRIPTS_ROOT", "/etc/netbox/scripts")
277 |
278 | # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
279 | # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
280 | # database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
281 | SESSION_FILE_PATH = environ.get("SESSIONS_ROOT", None)
282 |
283 | # Time zone (default: UTC)
284 | TIME_ZONE = environ.get("TIME_ZONE", "UTC")
285 |
286 | # Date/time formatting. See the following link for supported formats:
287 | # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
288 | DATE_FORMAT = environ.get("DATE_FORMAT", "N j, Y")
289 | SHORT_DATE_FORMAT = environ.get("SHORT_DATE_FORMAT", "Y-m-d")
290 | TIME_FORMAT = environ.get("TIME_FORMAT", "g:i a")
291 | SHORT_TIME_FORMAT = environ.get("SHORT_TIME_FORMAT", "H:i:s")
292 | DATETIME_FORMAT = environ.get("DATETIME_FORMAT", "N j, Y g:i a")
293 | SHORT_DATETIME_FORMAT = environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i")
294 |
--------------------------------------------------------------------------------