netbox-windows-dhcp-plugin 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netbox_windows_dhcp_plugin/__init__.py +44 -0
- netbox_windows_dhcp_plugin/api/__init__.py +0 -0
- netbox_windows_dhcp_plugin/api/serializers.py +362 -0
- netbox_windows_dhcp_plugin/api/urls.py +25 -0
- netbox_windows_dhcp_plugin/api/views.py +196 -0
- netbox_windows_dhcp_plugin/choices.py +222 -0
- netbox_windows_dhcp_plugin/conf.py +171 -0
- netbox_windows_dhcp_plugin/connection.py +113 -0
- netbox_windows_dhcp_plugin/filtersets.py +348 -0
- netbox_windows_dhcp_plugin/forms.py +884 -0
- netbox_windows_dhcp_plugin/graphql/__init__.py +3 -0
- netbox_windows_dhcp_plugin/graphql/filters.py +133 -0
- netbox_windows_dhcp_plugin/graphql/schema.py +60 -0
- netbox_windows_dhcp_plugin/graphql/types.py +179 -0
- netbox_windows_dhcp_plugin/import_status.py +314 -0
- netbox_windows_dhcp_plugin/integrations/__init__.py +0 -0
- netbox_windows_dhcp_plugin/integrations/branching.py +571 -0
- netbox_windows_dhcp_plugin/jobs.py +603 -0
- netbox_windows_dhcp_plugin/leases.py +59 -0
- netbox_windows_dhcp_plugin/migrations/0001_initial.py +837 -0
- netbox_windows_dhcp_plugin/migrations/0002_import_fields.py +31 -0
- netbox_windows_dhcp_plugin/migrations/0003_remaining_objects.py +193 -0
- netbox_windows_dhcp_plugin/migrations/0004_reservation_iaid.py +18 -0
- netbox_windows_dhcp_plugin/migrations/0005_syncrun_destination.py +15 -0
- netbox_windows_dhcp_plugin/migrations/0006_windowsserver_reported_names.py +18 -0
- netbox_windows_dhcp_plugin/migrations/__init__.py +0 -0
- netbox_windows_dhcp_plugin/models/__init__.py +36 -0
- netbox_windows_dhcp_plugin/models/dhcp.py +821 -0
- netbox_windows_dhcp_plugin/models/servers.py +254 -0
- netbox_windows_dhcp_plugin/models/sync.py +223 -0
- netbox_windows_dhcp_plugin/navigation.py +64 -0
- netbox_windows_dhcp_plugin/object_actions.py +142 -0
- netbox_windows_dhcp_plugin/review.py +58 -0
- netbox_windows_dhcp_plugin/search.py +56 -0
- netbox_windows_dhcp_plugin/signals.py +270 -0
- netbox_windows_dhcp_plugin/sync/__init__.py +7 -0
- netbox_windows_dhcp_plugin/sync/diff.py +134 -0
- netbox_windows_dhcp_plugin/sync/importer.py +518 -0
- netbox_windows_dhcp_plugin/sync/mapping.py +812 -0
- netbox_windows_dhcp_plugin/sync/netbox_state.py +568 -0
- netbox_windows_dhcp_plugin/sync/push.py +826 -0
- netbox_windows_dhcp_plugin/sync/pusher.py +216 -0
- netbox_windows_dhcp_plugin/sync/windows_state.py +855 -0
- netbox_windows_dhcp_plugin/sync/windows_writer.py +571 -0
- netbox_windows_dhcp_plugin/tables.py +430 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/buttons/bulk_post.html +3 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/buttons/import.html +56 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/buttons/post.html +6 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/import_main_confirm.html +26 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/import_branch_outcome.html +27 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/import_branch_rows.html +30 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/import_status.html +165 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/lease_panel.html +83 -0
- netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/subnet_leases.html +9 -0
- netbox_windows_dhcp_plugin/ui/__init__.py +0 -0
- netbox_windows_dhcp_plugin/ui/panels.py +201 -0
- netbox_windows_dhcp_plugin/urls.py +32 -0
- netbox_windows_dhcp_plugin/version.py +1 -0
- netbox_windows_dhcp_plugin/views.py +741 -0
- netbox_windows_dhcp_plugin/windows/__init__.py +18 -0
- netbox_windows_dhcp_plugin/windows/client.py +2105 -0
- netbox_windows_dhcp_plugin/windows/fake.py +1307 -0
- netbox_windows_dhcp_plugin/windows/normalize.py +251 -0
- netbox_windows_dhcp_plugin/windows/psscript.py +316 -0
- netbox_windows_dhcp_plugin/windows/runner.py +130 -0
- netbox_windows_dhcp_plugin/windows/ssh.py +319 -0
- netbox_windows_dhcp_plugin/windows/winrm.py +148 -0
- netbox_windows_dhcp_plugin-0.1.0.dist-info/METADATA +137 -0
- netbox_windows_dhcp_plugin-0.1.0.dist-info/RECORD +72 -0
- netbox_windows_dhcp_plugin-0.1.0.dist-info/WHEEL +4 -0
- netbox_windows_dhcp_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- netbox_windows_dhcp_plugin-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .version import __version__
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from netbox.plugins import PluginConfig
|
|
5
|
+
except ImportError: # imported outside NetBox (pure unit tests, build tools)
|
|
6
|
+
PluginConfig = None
|
|
7
|
+
|
|
8
|
+
if PluginConfig is not None:
|
|
9
|
+
from .conf import DEFAULTS
|
|
10
|
+
|
|
11
|
+
class WindowsDHCPConfig(PluginConfig):
|
|
12
|
+
name = "netbox_windows_dhcp_plugin"
|
|
13
|
+
verbose_name = "Windows DHCP"
|
|
14
|
+
description = "Sync netbox-plugin-dhcp data with Microsoft Windows DHCP servers"
|
|
15
|
+
version = __version__
|
|
16
|
+
author = "Thomas Christory"
|
|
17
|
+
author_email = "9317624+thomaschristory@users.noreply.github.com"
|
|
18
|
+
base_url = "windows-dhcp"
|
|
19
|
+
min_version = "4.7.0"
|
|
20
|
+
max_version = "4.7.99"
|
|
21
|
+
required_settings = []
|
|
22
|
+
default_settings = DEFAULTS
|
|
23
|
+
|
|
24
|
+
def ready(self):
|
|
25
|
+
super().ready()
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
from . import (
|
|
29
|
+
jobs, # noqa: F401 - registers the import scheduler system job
|
|
30
|
+
signals,
|
|
31
|
+
views, # noqa: F401 - the Leases tab on subnets, registered before netbox-plugin-dhcp builds its URLs
|
|
32
|
+
)
|
|
33
|
+
from .conf import profiles_without_allowed_hosts
|
|
34
|
+
from .integrations import branching
|
|
35
|
+
|
|
36
|
+
branching.register()
|
|
37
|
+
signals.connect()
|
|
38
|
+
unrestricted = profiles_without_allowed_hosts()
|
|
39
|
+
if unrestricted:
|
|
40
|
+
logging.getLogger(__name__).warning(
|
|
41
|
+
"credential profiles without allowed_hosts can be sent to any host: %s", ", ".join(unrestricted)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
config = WindowsDHCPConfig
|
|
File without changes
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
from core.api.serializers_.jobs import JobSerializer
|
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
from drf_spectacular.utils import extend_schema_field
|
|
4
|
+
from ipam.api.serializers import VRFSerializer
|
|
5
|
+
from netbox.api.fields import ChoiceField, ContentTypeField
|
|
6
|
+
from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer
|
|
7
|
+
from netbox_dhcp.api.serializers import (
|
|
8
|
+
DHCPServerSerializer,
|
|
9
|
+
HostReservationSerializer,
|
|
10
|
+
OptionDefinitionSerializer,
|
|
11
|
+
OptionSerializer,
|
|
12
|
+
SubnetSerializer,
|
|
13
|
+
)
|
|
14
|
+
from rest_framework import serializers
|
|
15
|
+
from utilities.api import get_serializer_for_model
|
|
16
|
+
|
|
17
|
+
from ..choices import (
|
|
18
|
+
ClassTypeChoices,
|
|
19
|
+
FailoverModeChoices,
|
|
20
|
+
FailoverServerRoleChoices,
|
|
21
|
+
ImportDestinationChoices,
|
|
22
|
+
MacFilterListChoices,
|
|
23
|
+
PolicyConditionChoices,
|
|
24
|
+
ProposalItemActionChoices,
|
|
25
|
+
ProposalItemStatusChoices,
|
|
26
|
+
ProposalStatusChoices,
|
|
27
|
+
ReservationTypeChoices,
|
|
28
|
+
ScopeStateChoices,
|
|
29
|
+
ScopeTypeChoices,
|
|
30
|
+
SyncKindChoices,
|
|
31
|
+
SyncStatusChoices,
|
|
32
|
+
TransportChoices,
|
|
33
|
+
WinRMAuthChoices,
|
|
34
|
+
)
|
|
35
|
+
from ..models import (
|
|
36
|
+
ExclusionRange,
|
|
37
|
+
FailoverRelationship,
|
|
38
|
+
ImportProposal,
|
|
39
|
+
ImportProposalItem,
|
|
40
|
+
MacFilter,
|
|
41
|
+
MacFilterLists,
|
|
42
|
+
SyncRun,
|
|
43
|
+
WindowsClass,
|
|
44
|
+
WindowsOptionDefinition,
|
|
45
|
+
WindowsOptionQualifier,
|
|
46
|
+
WindowsPolicy,
|
|
47
|
+
WindowsReservation,
|
|
48
|
+
WindowsScope,
|
|
49
|
+
WindowsServer,
|
|
50
|
+
WindowsVendorOption,
|
|
51
|
+
WindowsVendorOptionDefinition,
|
|
52
|
+
)
|
|
53
|
+
from ..models.dhcp import VENDOR_OPTION_PARENTS
|
|
54
|
+
|
|
55
|
+
API = "plugins-api:netbox_windows_dhcp_plugin-api"
|
|
56
|
+
COMMON = ("id", "url", "display_url", "display")
|
|
57
|
+
TAIL = ("tags", "custom_fields", "created", "last_updated")
|
|
58
|
+
PRIMARY_TAIL = ("description", "comments", "owner", *TAIL)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _url(model_name):
|
|
62
|
+
return serializers.HyperlinkedIdentityField(view_name=f"{API}:{model_name}-detail")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WindowsServerSerializer(PrimaryModelSerializer):
|
|
66
|
+
url = _url("windowsserver")
|
|
67
|
+
dhcp_server = DHCPServerSerializer(nested=True)
|
|
68
|
+
transport = ChoiceField(choices=TransportChoices, required=False)
|
|
69
|
+
winrm_auth = ChoiceField(choices=WinRMAuthChoices, required=False)
|
|
70
|
+
import_vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
|
71
|
+
|
|
72
|
+
class Meta:
|
|
73
|
+
model = WindowsServer
|
|
74
|
+
fields = (
|
|
75
|
+
*COMMON,
|
|
76
|
+
"dhcp_server",
|
|
77
|
+
"host",
|
|
78
|
+
"transport",
|
|
79
|
+
"port",
|
|
80
|
+
"credential_profile",
|
|
81
|
+
"winrm_auth",
|
|
82
|
+
"winrm_https",
|
|
83
|
+
"verify_tls",
|
|
84
|
+
"kerberos_realm",
|
|
85
|
+
"kerberos_spn",
|
|
86
|
+
"ssh_host_key",
|
|
87
|
+
"computer_name",
|
|
88
|
+
"reported_names",
|
|
89
|
+
"import_vrf",
|
|
90
|
+
"sync_enabled",
|
|
91
|
+
"import_interval",
|
|
92
|
+
*PRIMARY_TAIL,
|
|
93
|
+
)
|
|
94
|
+
brief_fields = ("id", "url", "display", "host", "description")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class WindowsScopeSerializer(NetBoxModelSerializer):
|
|
98
|
+
url = _url("windowsscope")
|
|
99
|
+
subnet = SubnetSerializer(nested=True)
|
|
100
|
+
state = ChoiceField(choices=ScopeStateChoices, required=False)
|
|
101
|
+
scope_type = ChoiceField(choices=ScopeTypeChoices, required=False)
|
|
102
|
+
|
|
103
|
+
class Meta:
|
|
104
|
+
model = WindowsScope
|
|
105
|
+
fields = (
|
|
106
|
+
*COMMON,
|
|
107
|
+
"subnet",
|
|
108
|
+
"state",
|
|
109
|
+
"scope_type",
|
|
110
|
+
"delay_ms",
|
|
111
|
+
"max_bootp_clients",
|
|
112
|
+
"activate_policies",
|
|
113
|
+
"preference",
|
|
114
|
+
*TAIL,
|
|
115
|
+
)
|
|
116
|
+
brief_fields = ("id", "url", "display", "state")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class WindowsReservationSerializer(NetBoxModelSerializer):
|
|
120
|
+
url = _url("windowsreservation")
|
|
121
|
+
host_reservation = HostReservationSerializer(nested=True)
|
|
122
|
+
reservation_type = ChoiceField(choices=ReservationTypeChoices, required=False)
|
|
123
|
+
|
|
124
|
+
class Meta:
|
|
125
|
+
model = WindowsReservation
|
|
126
|
+
fields = (*COMMON, "host_reservation", "reservation_type", "iaid", *TAIL)
|
|
127
|
+
brief_fields = ("id", "url", "display", "reservation_type")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ExclusionRangeSerializer(PrimaryModelSerializer):
|
|
131
|
+
url = _url("exclusionrange")
|
|
132
|
+
subnet = SubnetSerializer(nested=True)
|
|
133
|
+
|
|
134
|
+
class Meta:
|
|
135
|
+
model = ExclusionRange
|
|
136
|
+
fields = (*COMMON, "subnet", "start_address", "end_address", *PRIMARY_TAIL)
|
|
137
|
+
brief_fields = ("id", "url", "display", "start_address", "end_address", "description")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class WindowsClassSerializer(PrimaryModelSerializer):
|
|
141
|
+
url = _url("windowsclass")
|
|
142
|
+
dhcp_server = DHCPServerSerializer(nested=True)
|
|
143
|
+
class_type = ChoiceField(choices=ClassTypeChoices)
|
|
144
|
+
|
|
145
|
+
class Meta:
|
|
146
|
+
model = WindowsClass
|
|
147
|
+
fields = (*COMMON, "dhcp_server", "name", "class_type", "family", "data", *PRIMARY_TAIL)
|
|
148
|
+
brief_fields = ("id", "url", "display", "name", "class_type", "description")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class WindowsOptionDefinitionSerializer(NetBoxModelSerializer):
|
|
152
|
+
url = _url("windowsoptiondefinition")
|
|
153
|
+
option_definition = OptionDefinitionSerializer(nested=True)
|
|
154
|
+
|
|
155
|
+
class Meta:
|
|
156
|
+
model = WindowsOptionDefinition
|
|
157
|
+
fields = (*COMMON, "option_definition", "windows_type", "default_value", *TAIL)
|
|
158
|
+
brief_fields = ("id", "url", "display")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class WindowsPolicySerializer(PrimaryModelSerializer):
|
|
162
|
+
url = _url("windowspolicy")
|
|
163
|
+
dhcp_server = DHCPServerSerializer(nested=True)
|
|
164
|
+
subnet = SubnetSerializer(nested=True, required=False, allow_null=True)
|
|
165
|
+
condition = ChoiceField(choices=PolicyConditionChoices, required=False)
|
|
166
|
+
|
|
167
|
+
class Meta:
|
|
168
|
+
model = WindowsPolicy
|
|
169
|
+
fields = (
|
|
170
|
+
*COMMON,
|
|
171
|
+
"dhcp_server",
|
|
172
|
+
"subnet",
|
|
173
|
+
"name",
|
|
174
|
+
"processing_order",
|
|
175
|
+
"enabled",
|
|
176
|
+
"condition",
|
|
177
|
+
"criteria",
|
|
178
|
+
"lease_duration",
|
|
179
|
+
"ip_ranges",
|
|
180
|
+
*PRIMARY_TAIL,
|
|
181
|
+
)
|
|
182
|
+
brief_fields = ("id", "url", "display", "name", "description")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class WindowsOptionQualifierSerializer(NetBoxModelSerializer):
|
|
186
|
+
url = _url("windowsoptionqualifier")
|
|
187
|
+
option = OptionSerializer(nested=True)
|
|
188
|
+
user_class = WindowsClassSerializer(nested=True, required=False, allow_null=True)
|
|
189
|
+
policy = WindowsPolicySerializer(nested=True, required=False, allow_null=True)
|
|
190
|
+
|
|
191
|
+
class Meta:
|
|
192
|
+
model = WindowsOptionQualifier
|
|
193
|
+
fields = (*COMMON, "option", "user_class", "policy", *TAIL)
|
|
194
|
+
brief_fields = ("id", "url", "display")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class FailoverRelationshipSerializer(PrimaryModelSerializer):
|
|
198
|
+
url = _url("failoverrelationship")
|
|
199
|
+
primary_server = DHCPServerSerializer(nested=True)
|
|
200
|
+
partner_server = DHCPServerSerializer(nested=True)
|
|
201
|
+
mode = ChoiceField(choices=FailoverModeChoices, required=False)
|
|
202
|
+
server_role = ChoiceField(choices=FailoverServerRoleChoices, required=False, allow_blank=True)
|
|
203
|
+
subnets = SubnetSerializer(nested=True, many=True, required=False)
|
|
204
|
+
|
|
205
|
+
class Meta:
|
|
206
|
+
model = FailoverRelationship
|
|
207
|
+
fields = (
|
|
208
|
+
*COMMON,
|
|
209
|
+
"name",
|
|
210
|
+
"primary_server",
|
|
211
|
+
"partner_server",
|
|
212
|
+
"mode",
|
|
213
|
+
"max_client_lead_time",
|
|
214
|
+
"load_balance_percent",
|
|
215
|
+
"reserve_percent",
|
|
216
|
+
"server_role",
|
|
217
|
+
"state_switch_interval",
|
|
218
|
+
"shared_secret_profile",
|
|
219
|
+
"subnets",
|
|
220
|
+
*PRIMARY_TAIL,
|
|
221
|
+
)
|
|
222
|
+
brief_fields = ("id", "url", "display", "name", "mode", "description")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class MacFilterSerializer(PrimaryModelSerializer):
|
|
226
|
+
url = _url("macfilter")
|
|
227
|
+
dhcp_server = DHCPServerSerializer(nested=True)
|
|
228
|
+
list = ChoiceField(choices=MacFilterListChoices)
|
|
229
|
+
|
|
230
|
+
class Meta:
|
|
231
|
+
model = MacFilter
|
|
232
|
+
fields = (*COMMON, "dhcp_server", "mac_address", "list", *PRIMARY_TAIL)
|
|
233
|
+
brief_fields = ("id", "url", "display", "mac_address", "list", "description")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class MacFilterListsSerializer(NetBoxModelSerializer):
|
|
237
|
+
url = _url("macfilterlists")
|
|
238
|
+
dhcp_server = DHCPServerSerializer(nested=True)
|
|
239
|
+
|
|
240
|
+
class Meta:
|
|
241
|
+
model = MacFilterLists
|
|
242
|
+
fields = (*COMMON, "dhcp_server", "allow_list_enabled", "deny_list_enabled", *TAIL)
|
|
243
|
+
brief_fields = ("id", "url", "display", "allow_list_enabled", "deny_list_enabled")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class WindowsVendorOptionDefinitionSerializer(PrimaryModelSerializer):
|
|
247
|
+
url = _url("windowsvendoroptiondefinition")
|
|
248
|
+
vendor_class = WindowsClassSerializer(nested=True)
|
|
249
|
+
|
|
250
|
+
class Meta:
|
|
251
|
+
model = WindowsVendorOptionDefinition
|
|
252
|
+
fields = (
|
|
253
|
+
*COMMON,
|
|
254
|
+
"vendor_class",
|
|
255
|
+
"code",
|
|
256
|
+
"name",
|
|
257
|
+
"windows_type",
|
|
258
|
+
"multi_valued",
|
|
259
|
+
"default_value",
|
|
260
|
+
*PRIMARY_TAIL,
|
|
261
|
+
)
|
|
262
|
+
brief_fields = ("id", "url", "display", "code", "name", "description")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class WindowsVendorOptionSerializer(NetBoxModelSerializer):
|
|
266
|
+
url = _url("windowsvendoroption")
|
|
267
|
+
definition = WindowsVendorOptionDefinitionSerializer(nested=True)
|
|
268
|
+
# Written as "app_label.model" plus assigned_object_id, read back with the parent nested, like netbox-plugin-dhcp
|
|
269
|
+
# options.
|
|
270
|
+
assigned_object_type = ContentTypeField(queryset=ContentType.objects.filter(VENDOR_OPTION_PARENTS))
|
|
271
|
+
assigned_object = serializers.SerializerMethodField(read_only=True)
|
|
272
|
+
user_class = WindowsClassSerializer(nested=True, required=False, allow_null=True)
|
|
273
|
+
policy = WindowsPolicySerializer(nested=True, required=False, allow_null=True)
|
|
274
|
+
|
|
275
|
+
class Meta:
|
|
276
|
+
model = WindowsVendorOption
|
|
277
|
+
fields = (
|
|
278
|
+
*COMMON,
|
|
279
|
+
"definition",
|
|
280
|
+
"assigned_object_type",
|
|
281
|
+
"assigned_object_id",
|
|
282
|
+
"assigned_object",
|
|
283
|
+
"user_class",
|
|
284
|
+
"policy",
|
|
285
|
+
"value",
|
|
286
|
+
*TAIL,
|
|
287
|
+
)
|
|
288
|
+
brief_fields = ("id", "url", "display", "value")
|
|
289
|
+
|
|
290
|
+
@extend_schema_field(serializers.JSONField(allow_null=True))
|
|
291
|
+
def get_assigned_object(self, instance):
|
|
292
|
+
if instance.assigned_object is None:
|
|
293
|
+
return None
|
|
294
|
+
serializer = get_serializer_for_model(instance.assigned_object)
|
|
295
|
+
return serializer(instance.assigned_object, nested=True, context={"request": self.context["request"]}).data
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class SyncRunSerializer(NetBoxModelSerializer):
|
|
299
|
+
url = _url("syncrun")
|
|
300
|
+
windows_server = WindowsServerSerializer(nested=True, read_only=True)
|
|
301
|
+
kind = ChoiceField(choices=SyncKindChoices, read_only=True)
|
|
302
|
+
status = ChoiceField(choices=SyncStatusChoices, read_only=True)
|
|
303
|
+
destination = ChoiceField(choices=ImportDestinationChoices, read_only=True)
|
|
304
|
+
job = JobSerializer(nested=True, read_only=True)
|
|
305
|
+
|
|
306
|
+
class Meta:
|
|
307
|
+
model = SyncRun
|
|
308
|
+
fields = (
|
|
309
|
+
*COMMON,
|
|
310
|
+
"windows_server",
|
|
311
|
+
"kind",
|
|
312
|
+
"status",
|
|
313
|
+
"job",
|
|
314
|
+
"started",
|
|
315
|
+
"completed",
|
|
316
|
+
"destination",
|
|
317
|
+
"branch_name",
|
|
318
|
+
"summary",
|
|
319
|
+
"error",
|
|
320
|
+
*TAIL,
|
|
321
|
+
)
|
|
322
|
+
read_only_fields = ("started", "completed", "branch_name", "summary", "error")
|
|
323
|
+
brief_fields = ("id", "url", "display", "kind", "status")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ImportProposalSerializer(NetBoxModelSerializer):
|
|
327
|
+
url = _url("importproposal")
|
|
328
|
+
windows_server = WindowsServerSerializer(nested=True, read_only=True)
|
|
329
|
+
sync_run = SyncRunSerializer(nested=True, read_only=True)
|
|
330
|
+
status = ChoiceField(choices=ProposalStatusChoices, read_only=True)
|
|
331
|
+
|
|
332
|
+
class Meta:
|
|
333
|
+
model = ImportProposal
|
|
334
|
+
fields = (*COMMON, "windows_server", "sync_run", "status", *TAIL)
|
|
335
|
+
brief_fields = ("id", "url", "display", "status")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class ImportProposalItemSerializer(NetBoxModelSerializer):
|
|
339
|
+
url = _url("importproposalitem")
|
|
340
|
+
proposal = ImportProposalSerializer(nested=True, read_only=True)
|
|
341
|
+
action = ChoiceField(choices=ProposalItemActionChoices, read_only=True)
|
|
342
|
+
status = ChoiceField(choices=ProposalItemStatusChoices, read_only=True)
|
|
343
|
+
object_type = ContentTypeField(read_only=True)
|
|
344
|
+
|
|
345
|
+
class Meta:
|
|
346
|
+
model = ImportProposalItem
|
|
347
|
+
fields = (
|
|
348
|
+
*COMMON,
|
|
349
|
+
"proposal",
|
|
350
|
+
"action",
|
|
351
|
+
"status",
|
|
352
|
+
"kind",
|
|
353
|
+
"key",
|
|
354
|
+
"object_type",
|
|
355
|
+
"object_id",
|
|
356
|
+
"before",
|
|
357
|
+
"after",
|
|
358
|
+
"error",
|
|
359
|
+
*TAIL,
|
|
360
|
+
)
|
|
361
|
+
read_only_fields = ("kind", "key", "object_id", "before", "after", "error")
|
|
362
|
+
brief_fields = ("id", "url", "display", "action", "status")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from netbox.api.routers import NetBoxRouter
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
app_name = "netbox_windows_dhcp_plugin"
|
|
6
|
+
|
|
7
|
+
router = NetBoxRouter()
|
|
8
|
+
router.register("windows-servers", views.WindowsServerViewSet)
|
|
9
|
+
router.register("windows-scopes", views.WindowsScopeViewSet)
|
|
10
|
+
router.register("windows-reservations", views.WindowsReservationViewSet)
|
|
11
|
+
router.register("exclusion-ranges", views.ExclusionRangeViewSet)
|
|
12
|
+
router.register("windows-classes", views.WindowsClassViewSet)
|
|
13
|
+
router.register("windows-option-definitions", views.WindowsOptionDefinitionViewSet)
|
|
14
|
+
router.register("windows-policies", views.WindowsPolicyViewSet)
|
|
15
|
+
router.register("windows-option-qualifiers", views.WindowsOptionQualifierViewSet)
|
|
16
|
+
router.register("failover-relationships", views.FailoverRelationshipViewSet)
|
|
17
|
+
router.register("mac-filters", views.MacFilterViewSet)
|
|
18
|
+
router.register("mac-filter-lists", views.MacFilterListsViewSet)
|
|
19
|
+
router.register("vendor-option-definitions", views.WindowsVendorOptionDefinitionViewSet)
|
|
20
|
+
router.register("vendor-options", views.WindowsVendorOptionViewSet)
|
|
21
|
+
router.register("sync-runs", views.SyncRunViewSet)
|
|
22
|
+
router.register("import-proposals", views.ImportProposalViewSet)
|
|
23
|
+
router.register("import-proposal-items", views.ImportProposalItemViewSet)
|
|
24
|
+
|
|
25
|
+
urlpatterns = router.urls
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from core.api.serializers import JobSerializer
|
|
2
|
+
from django.db.models import Count
|
|
3
|
+
from netbox.api.authentication import TokenWritePermission
|
|
4
|
+
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
|
5
|
+
from rest_framework.decorators import action
|
|
6
|
+
from rest_framework.exceptions import PermissionDenied, ValidationError
|
|
7
|
+
from rest_framework.response import Response
|
|
8
|
+
|
|
9
|
+
from .. import filtersets, models, review
|
|
10
|
+
from . import serializers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _viewset(model, queryset, base=NetBoxModelViewSet):
|
|
14
|
+
name = model.__name__
|
|
15
|
+
return type(
|
|
16
|
+
f"{name}ViewSet",
|
|
17
|
+
(base,),
|
|
18
|
+
{
|
|
19
|
+
"queryset": queryset,
|
|
20
|
+
"serializer_class": getattr(serializers, f"{name}Serializer"),
|
|
21
|
+
"filterset_class": getattr(filtersets, f"{name}FilterSet"),
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WindowsServerViewSet(_viewset(models.WindowsServer, models.WindowsServer.objects.select_related("dhcp_server"))):
|
|
27
|
+
def get_permissions(self):
|
|
28
|
+
# NetBox maps POST to the "add" permission. A connection test needs "change" on the server instead,
|
|
29
|
+
# enforced below; here only check that the token may write (as NetBox does for render-config).
|
|
30
|
+
if self.action in ("test_connection", "run_import", "push", "dry_run"):
|
|
31
|
+
return [TokenWritePermission()]
|
|
32
|
+
return super().get_permissions()
|
|
33
|
+
|
|
34
|
+
def _changeable_server(self, request):
|
|
35
|
+
# A server the user may not change is reported as not found, like any restricted object.
|
|
36
|
+
self.queryset = models.WindowsServer.objects.restrict(request.user, "change").restrict(request.user, "view")
|
|
37
|
+
return self.get_object()
|
|
38
|
+
|
|
39
|
+
@action(detail=True, methods=["post"], url_path="test-connection")
|
|
40
|
+
def test_connection(self, request, pk):
|
|
41
|
+
"""Queue a connection test and return the job."""
|
|
42
|
+
from ..jobs import TestConnectionJob
|
|
43
|
+
|
|
44
|
+
job = TestConnectionJob.enqueue(instance=self._changeable_server(request), user=request.user)
|
|
45
|
+
return Response(JobSerializer(job, context={"request": request}).data, status=202)
|
|
46
|
+
|
|
47
|
+
@action(detail=True, methods=["post"], url_path="import")
|
|
48
|
+
def run_import(self, request, pk):
|
|
49
|
+
"""
|
|
50
|
+
Queue an import from the server and return the job (409 if one is already queued or running).
|
|
51
|
+
|
|
52
|
+
``destination`` chooses where it writes: "branch" (a new or open import branch, the default with
|
|
53
|
+
netbox-branching), "current" (the branch named in ``branch``, or the request's active branch) or "main" with
|
|
54
|
+
it, "proposal" without it. A destination that is unknown here is a 400; one the user may not use is a 403.
|
|
55
|
+
"""
|
|
56
|
+
from ..integrations import branching
|
|
57
|
+
from ..jobs import ImportRefused, enqueue_import, import_refusal
|
|
58
|
+
|
|
59
|
+
server = self._changeable_server(request)
|
|
60
|
+
data = request.data if hasattr(request.data, "get") else {}
|
|
61
|
+
destination = data.get("destination") or branching.import_destinations()[0]
|
|
62
|
+
if destination not in branching.import_destinations():
|
|
63
|
+
raise ValidationError({"destination": f"choose one of: {', '.join(branching.import_destinations())}"})
|
|
64
|
+
branch = ""
|
|
65
|
+
if destination == "current":
|
|
66
|
+
branch = data.get("branch") or branching.active_branch_name() or ""
|
|
67
|
+
if not branch:
|
|
68
|
+
raise ValidationError({"branch": "name the branch to import into, or send the request in a branch"})
|
|
69
|
+
missing = branching.missing_import_permissions(request.user, destination)
|
|
70
|
+
if missing:
|
|
71
|
+
raise PermissionDenied(f"importing needs the permission: {', '.join(missing)}")
|
|
72
|
+
refusal = import_refusal(destination, request.user, branch)
|
|
73
|
+
if refusal:
|
|
74
|
+
raise PermissionDenied(refusal)
|
|
75
|
+
try:
|
|
76
|
+
job = enqueue_import(server, request.user, destination, branch)
|
|
77
|
+
except ImportRefused:
|
|
78
|
+
# Allowed a moment ago (a setting or permission changed in between): the next try says why.
|
|
79
|
+
raise PermissionDenied("the import was refused; try again") from None
|
|
80
|
+
if job is None:
|
|
81
|
+
return Response({"detail": "an import from this server is already queued or running"}, status=409)
|
|
82
|
+
return Response(JobSerializer(job, context={"request": request}).data, status=202)
|
|
83
|
+
|
|
84
|
+
def _push(self, request, dry_run):
|
|
85
|
+
from ..jobs import enqueue_push
|
|
86
|
+
|
|
87
|
+
server = self._changeable_server(request)
|
|
88
|
+
if not dry_run and not server.sync_enabled:
|
|
89
|
+
return Response({"detail": "sync is disabled for this server"}, status=409)
|
|
90
|
+
job = enqueue_push(server, request.user, dry_run=dry_run)
|
|
91
|
+
if job is None:
|
|
92
|
+
what = "a dry run" if dry_run else "a push"
|
|
93
|
+
return Response({"detail": f"{what} for this server is already queued"}, status=409)
|
|
94
|
+
return Response(JobSerializer(job, context={"request": request}).data, status=202)
|
|
95
|
+
|
|
96
|
+
@action(detail=True, methods=["post"])
|
|
97
|
+
def push(self, request, pk):
|
|
98
|
+
"""Queue a push to the server and return the job (409 if one is already waiting to run)."""
|
|
99
|
+
return self._push(request, dry_run=False)
|
|
100
|
+
|
|
101
|
+
@action(detail=True, methods=["post"], url_path="dry-run")
|
|
102
|
+
def dry_run(self, request, pk):
|
|
103
|
+
"""Queue a dry run (plan a push without writing) and return the job."""
|
|
104
|
+
return self._push(request, dry_run=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
WindowsScopeViewSet = _viewset(models.WindowsScope, models.WindowsScope.objects.select_related("subnet"))
|
|
108
|
+
WindowsReservationViewSet = _viewset(
|
|
109
|
+
models.WindowsReservation, models.WindowsReservation.objects.select_related("host_reservation")
|
|
110
|
+
)
|
|
111
|
+
ExclusionRangeViewSet = _viewset(models.ExclusionRange, models.ExclusionRange.objects.select_related("subnet"))
|
|
112
|
+
WindowsClassViewSet = _viewset(models.WindowsClass, models.WindowsClass.objects.select_related("dhcp_server"))
|
|
113
|
+
WindowsOptionDefinitionViewSet = _viewset(models.WindowsOptionDefinition, models.WindowsOptionDefinition.objects.all())
|
|
114
|
+
WindowsPolicyViewSet = _viewset(models.WindowsPolicy, models.WindowsPolicy.objects.select_related("dhcp_server"))
|
|
115
|
+
WindowsOptionQualifierViewSet = _viewset(models.WindowsOptionQualifier, models.WindowsOptionQualifier.objects.all())
|
|
116
|
+
FailoverRelationshipViewSet = _viewset(
|
|
117
|
+
models.FailoverRelationship, models.FailoverRelationship.objects.prefetch_related("subnets")
|
|
118
|
+
)
|
|
119
|
+
MacFilterViewSet = _viewset(models.MacFilter, models.MacFilter.objects.select_related("dhcp_server"))
|
|
120
|
+
MacFilterListsViewSet = _viewset(models.MacFilterLists, models.MacFilterLists.objects.select_related("dhcp_server"))
|
|
121
|
+
WindowsVendorOptionDefinitionViewSet = _viewset(
|
|
122
|
+
models.WindowsVendorOptionDefinition, models.WindowsVendorOptionDefinition.objects.select_related("vendor_class")
|
|
123
|
+
)
|
|
124
|
+
WindowsVendorOptionViewSet = _viewset(
|
|
125
|
+
models.WindowsVendorOption,
|
|
126
|
+
models.WindowsVendorOption.objects.select_related(
|
|
127
|
+
"definition__vendor_class", "assigned_object_type", "user_class", "policy"
|
|
128
|
+
).prefetch_related("assigned_object"),
|
|
129
|
+
)
|
|
130
|
+
SyncRunViewSet = _viewset(models.SyncRun, models.SyncRun.objects.all(), base=NetBoxReadOnlyModelViewSet)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _ReviewActionsMixin:
|
|
134
|
+
"""Apply or dismiss proposal items as the requesting user (see review.py for the permission rules)."""
|
|
135
|
+
|
|
136
|
+
def get_permissions(self):
|
|
137
|
+
# POST would map to "add"; these actions need "change", enforced through the restricted querysets below.
|
|
138
|
+
if self.action in ("apply", "dismiss"):
|
|
139
|
+
return [TokenWritePermission()]
|
|
140
|
+
return super().get_permissions()
|
|
141
|
+
|
|
142
|
+
def _review(self, request, items, dismiss):
|
|
143
|
+
items = review.open_items(items)
|
|
144
|
+
if dismiss:
|
|
145
|
+
return Response({"dismissed": review.dismiss(request.user, items)})
|
|
146
|
+
applied, failed, denied = review.apply(request.user, items)
|
|
147
|
+
return Response({"applied": applied, "failed": failed, "missing_permissions": denied})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# The item count is an aggregate, and Django drops Meta.ordering on aggregate queries, so order explicitly
|
|
151
|
+
# (the API paginator rejects an unordered queryset).
|
|
152
|
+
class ImportProposalViewSet(
|
|
153
|
+
_ReviewActionsMixin,
|
|
154
|
+
_viewset(
|
|
155
|
+
models.ImportProposal,
|
|
156
|
+
models.ImportProposal.objects.annotate(item_count=Count("items")).order_by("-created"),
|
|
157
|
+
base=NetBoxReadOnlyModelViewSet,
|
|
158
|
+
),
|
|
159
|
+
):
|
|
160
|
+
def _proposal(self, request):
|
|
161
|
+
# NetBox has already restricted the queryset to "add" (what POST maps to); start again from "change".
|
|
162
|
+
self.queryset = models.ImportProposal.objects.restrict(request.user, "change").restrict(request.user, "view")
|
|
163
|
+
return self.get_object()
|
|
164
|
+
|
|
165
|
+
@action(detail=True, methods=["post"])
|
|
166
|
+
def apply(self, request, pk):
|
|
167
|
+
"""Apply every pending item of the proposal."""
|
|
168
|
+
items = self._proposal(request).items.restrict(request.user, "change")
|
|
169
|
+
return self._review(request, items, dismiss=False)
|
|
170
|
+
|
|
171
|
+
@action(detail=True, methods=["post"])
|
|
172
|
+
def dismiss(self, request, pk):
|
|
173
|
+
"""Dismiss every pending item of the proposal."""
|
|
174
|
+
items = self._proposal(request).items.restrict(request.user, "change")
|
|
175
|
+
return self._review(request, items, dismiss=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ImportProposalItemViewSet(
|
|
179
|
+
_ReviewActionsMixin,
|
|
180
|
+
_viewset(models.ImportProposalItem, models.ImportProposalItem.objects.all(), base=NetBoxReadOnlyModelViewSet),
|
|
181
|
+
):
|
|
182
|
+
def _selected(self, request):
|
|
183
|
+
ids = request.data.get("ids") if isinstance(request.data, dict) else None
|
|
184
|
+
if not isinstance(ids, list) or not all(isinstance(i, int) for i in ids):
|
|
185
|
+
raise ValidationError({"ids": "a list of item IDs is required"})
|
|
186
|
+
return models.ImportProposalItem.objects.restrict(request.user, "change").filter(pk__in=ids)
|
|
187
|
+
|
|
188
|
+
@action(detail=False, methods=["post"])
|
|
189
|
+
def apply(self, request):
|
|
190
|
+
"""Apply the items listed in {"ids": [...]}."""
|
|
191
|
+
return self._review(request, self._selected(request), dismiss=False)
|
|
192
|
+
|
|
193
|
+
@action(detail=False, methods=["post"])
|
|
194
|
+
def dismiss(self, request):
|
|
195
|
+
"""Dismiss the items listed in {"ids": [...]}."""
|
|
196
|
+
return self._review(request, self._selected(request), dismiss=True)
|