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.
Files changed (72) hide show
  1. netbox_windows_dhcp_plugin/__init__.py +44 -0
  2. netbox_windows_dhcp_plugin/api/__init__.py +0 -0
  3. netbox_windows_dhcp_plugin/api/serializers.py +362 -0
  4. netbox_windows_dhcp_plugin/api/urls.py +25 -0
  5. netbox_windows_dhcp_plugin/api/views.py +196 -0
  6. netbox_windows_dhcp_plugin/choices.py +222 -0
  7. netbox_windows_dhcp_plugin/conf.py +171 -0
  8. netbox_windows_dhcp_plugin/connection.py +113 -0
  9. netbox_windows_dhcp_plugin/filtersets.py +348 -0
  10. netbox_windows_dhcp_plugin/forms.py +884 -0
  11. netbox_windows_dhcp_plugin/graphql/__init__.py +3 -0
  12. netbox_windows_dhcp_plugin/graphql/filters.py +133 -0
  13. netbox_windows_dhcp_plugin/graphql/schema.py +60 -0
  14. netbox_windows_dhcp_plugin/graphql/types.py +179 -0
  15. netbox_windows_dhcp_plugin/import_status.py +314 -0
  16. netbox_windows_dhcp_plugin/integrations/__init__.py +0 -0
  17. netbox_windows_dhcp_plugin/integrations/branching.py +571 -0
  18. netbox_windows_dhcp_plugin/jobs.py +603 -0
  19. netbox_windows_dhcp_plugin/leases.py +59 -0
  20. netbox_windows_dhcp_plugin/migrations/0001_initial.py +837 -0
  21. netbox_windows_dhcp_plugin/migrations/0002_import_fields.py +31 -0
  22. netbox_windows_dhcp_plugin/migrations/0003_remaining_objects.py +193 -0
  23. netbox_windows_dhcp_plugin/migrations/0004_reservation_iaid.py +18 -0
  24. netbox_windows_dhcp_plugin/migrations/0005_syncrun_destination.py +15 -0
  25. netbox_windows_dhcp_plugin/migrations/0006_windowsserver_reported_names.py +18 -0
  26. netbox_windows_dhcp_plugin/migrations/__init__.py +0 -0
  27. netbox_windows_dhcp_plugin/models/__init__.py +36 -0
  28. netbox_windows_dhcp_plugin/models/dhcp.py +821 -0
  29. netbox_windows_dhcp_plugin/models/servers.py +254 -0
  30. netbox_windows_dhcp_plugin/models/sync.py +223 -0
  31. netbox_windows_dhcp_plugin/navigation.py +64 -0
  32. netbox_windows_dhcp_plugin/object_actions.py +142 -0
  33. netbox_windows_dhcp_plugin/review.py +58 -0
  34. netbox_windows_dhcp_plugin/search.py +56 -0
  35. netbox_windows_dhcp_plugin/signals.py +270 -0
  36. netbox_windows_dhcp_plugin/sync/__init__.py +7 -0
  37. netbox_windows_dhcp_plugin/sync/diff.py +134 -0
  38. netbox_windows_dhcp_plugin/sync/importer.py +518 -0
  39. netbox_windows_dhcp_plugin/sync/mapping.py +812 -0
  40. netbox_windows_dhcp_plugin/sync/netbox_state.py +568 -0
  41. netbox_windows_dhcp_plugin/sync/push.py +826 -0
  42. netbox_windows_dhcp_plugin/sync/pusher.py +216 -0
  43. netbox_windows_dhcp_plugin/sync/windows_state.py +855 -0
  44. netbox_windows_dhcp_plugin/sync/windows_writer.py +571 -0
  45. netbox_windows_dhcp_plugin/tables.py +430 -0
  46. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/buttons/bulk_post.html +3 -0
  47. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/buttons/import.html +56 -0
  48. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/buttons/post.html +6 -0
  49. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/import_main_confirm.html +26 -0
  50. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/import_branch_outcome.html +27 -0
  51. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/import_branch_rows.html +30 -0
  52. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/import_status.html +165 -0
  53. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/inc/lease_panel.html +83 -0
  54. netbox_windows_dhcp_plugin/templates/netbox_windows_dhcp_plugin/subnet_leases.html +9 -0
  55. netbox_windows_dhcp_plugin/ui/__init__.py +0 -0
  56. netbox_windows_dhcp_plugin/ui/panels.py +201 -0
  57. netbox_windows_dhcp_plugin/urls.py +32 -0
  58. netbox_windows_dhcp_plugin/version.py +1 -0
  59. netbox_windows_dhcp_plugin/views.py +741 -0
  60. netbox_windows_dhcp_plugin/windows/__init__.py +18 -0
  61. netbox_windows_dhcp_plugin/windows/client.py +2105 -0
  62. netbox_windows_dhcp_plugin/windows/fake.py +1307 -0
  63. netbox_windows_dhcp_plugin/windows/normalize.py +251 -0
  64. netbox_windows_dhcp_plugin/windows/psscript.py +316 -0
  65. netbox_windows_dhcp_plugin/windows/runner.py +130 -0
  66. netbox_windows_dhcp_plugin/windows/ssh.py +319 -0
  67. netbox_windows_dhcp_plugin/windows/winrm.py +148 -0
  68. netbox_windows_dhcp_plugin-0.1.0.dist-info/METADATA +137 -0
  69. netbox_windows_dhcp_plugin-0.1.0.dist-info/RECORD +72 -0
  70. netbox_windows_dhcp_plugin-0.1.0.dist-info/WHEEL +4 -0
  71. netbox_windows_dhcp_plugin-0.1.0.dist-info/entry_points.txt +2 -0
  72. 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)