serializers.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. from django.contrib.auth import get_user_model
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ObjectDoesNotExist
  4. from rest_framework import serializers
  5. from core.api.serializers import JobSerializer
  6. from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
  7. from dcim.api.nested_serializers import (
  8. NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
  9. NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
  10. )
  11. from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
  12. from drf_spectacular.utils import extend_schema_field
  13. from drf_spectacular.types import OpenApiTypes
  14. from extras.choices import *
  15. from extras.models import *
  16. from extras.utils import FeatureQuery
  17. from netbox.api.exceptions import SerializerNotFound
  18. from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
  19. from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
  20. from netbox.api.serializers.features import TaggableModelSerializer
  21. from netbox.constants import NESTED_SERIALIZER_PREFIX
  22. from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
  23. from tenancy.models import Tenant, TenantGroup
  24. from users.api.nested_serializers import NestedUserSerializer
  25. from utilities.api import get_serializer_for_model
  26. from virtualization.api.nested_serializers import (
  27. NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
  28. )
  29. from virtualization.models import Cluster, ClusterGroup, ClusterType
  30. from .nested_serializers import *
  31. __all__ = (
  32. 'BookmarkSerializer',
  33. 'ConfigContextSerializer',
  34. 'ConfigTemplateSerializer',
  35. 'ContentTypeSerializer',
  36. 'CustomFieldChoiceSetSerializer',
  37. 'CustomFieldSerializer',
  38. 'CustomLinkSerializer',
  39. 'DashboardSerializer',
  40. 'ExportTemplateSerializer',
  41. 'ImageAttachmentSerializer',
  42. 'JournalEntrySerializer',
  43. 'ObjectChangeSerializer',
  44. 'ReportDetailSerializer',
  45. 'ReportSerializer',
  46. 'ReportInputSerializer',
  47. 'SavedFilterSerializer',
  48. 'ScriptDetailSerializer',
  49. 'ScriptInputSerializer',
  50. 'ScriptLogMessageSerializer',
  51. 'ScriptOutputSerializer',
  52. 'ScriptSerializer',
  53. 'TagSerializer',
  54. 'WebhookSerializer',
  55. )
  56. #
  57. # Webhooks
  58. #
  59. class WebhookSerializer(NetBoxModelSerializer):
  60. url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
  61. content_types = ContentTypeField(
  62. queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
  63. many=True
  64. )
  65. class Meta:
  66. model = Webhook
  67. fields = [
  68. 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
  69. 'type_job_start', 'type_job_end', 'payload_url', 'enabled', 'http_method', 'http_content_type',
  70. 'additional_headers', 'body_template', 'secret', 'conditions', 'ssl_verification', 'ca_file_path',
  71. 'custom_fields', 'tags', 'created', 'last_updated',
  72. ]
  73. #
  74. # Custom fields
  75. #
  76. class CustomFieldSerializer(ValidatedModelSerializer):
  77. url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
  78. content_types = ContentTypeField(
  79. queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
  80. many=True
  81. )
  82. type = ChoiceField(choices=CustomFieldTypeChoices)
  83. object_type = ContentTypeField(
  84. queryset=ContentType.objects.all(),
  85. required=False
  86. )
  87. filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
  88. data_type = serializers.SerializerMethodField()
  89. choice_set = NestedCustomFieldChoiceSetSerializer(required=False)
  90. ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
  91. class Meta:
  92. model = CustomField
  93. fields = [
  94. 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
  95. 'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default',
  96. 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created',
  97. 'last_updated',
  98. ]
  99. def validate_type(self, value):
  100. if self.instance and self.instance.type != value:
  101. raise serializers.ValidationError('Changing the type of custom fields is not supported.')
  102. return value
  103. @extend_schema_field(OpenApiTypes.STR)
  104. def get_data_type(self, obj):
  105. types = CustomFieldTypeChoices
  106. if obj.type == types.TYPE_INTEGER:
  107. return 'integer'
  108. if obj.type == types.TYPE_DECIMAL:
  109. return 'decimal'
  110. if obj.type == types.TYPE_BOOLEAN:
  111. return 'boolean'
  112. if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
  113. return 'object'
  114. if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
  115. return 'array'
  116. return 'string'
  117. class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
  118. url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
  119. base_choices = ChoiceField(
  120. choices=CustomFieldChoiceSetBaseChoices,
  121. required=False
  122. )
  123. class Meta:
  124. model = CustomFieldChoiceSet
  125. fields = [
  126. 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
  127. 'choices_count', 'created', 'last_updated',
  128. ]
  129. #
  130. # Custom links
  131. #
  132. class CustomLinkSerializer(ValidatedModelSerializer):
  133. url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
  134. content_types = ContentTypeField(
  135. queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
  136. many=True
  137. )
  138. class Meta:
  139. model = CustomLink
  140. fields = [
  141. 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
  142. 'button_class', 'new_window', 'created', 'last_updated',
  143. ]
  144. #
  145. # Export templates
  146. #
  147. class ExportTemplateSerializer(ValidatedModelSerializer):
  148. url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
  149. content_types = ContentTypeField(
  150. queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
  151. many=True
  152. )
  153. data_source = NestedDataSourceSerializer(
  154. required=False
  155. )
  156. data_file = NestedDataFileSerializer(
  157. read_only=True
  158. )
  159. class Meta:
  160. model = ExportTemplate
  161. fields = [
  162. 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
  163. 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
  164. 'last_updated',
  165. ]
  166. #
  167. # Saved filters
  168. #
  169. class SavedFilterSerializer(ValidatedModelSerializer):
  170. url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
  171. content_types = ContentTypeField(
  172. queryset=ContentType.objects.all(),
  173. many=True
  174. )
  175. class Meta:
  176. model = SavedFilter
  177. fields = [
  178. 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
  179. 'shared', 'parameters', 'created', 'last_updated',
  180. ]
  181. #
  182. # Bookmarks
  183. #
  184. class BookmarkSerializer(ValidatedModelSerializer):
  185. url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
  186. object_type = ContentTypeField(
  187. queryset=ContentType.objects.filter(FeatureQuery('bookmarks').get_query()),
  188. )
  189. object = serializers.SerializerMethodField(read_only=True)
  190. user = NestedUserSerializer()
  191. class Meta:
  192. model = Bookmark
  193. fields = [
  194. 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
  195. ]
  196. @extend_schema_field(serializers.JSONField(allow_null=True))
  197. def get_object(self, instance):
  198. serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX)
  199. return serializer(instance.object, context={'request': self.context['request']}).data
  200. #
  201. # Tags
  202. #
  203. class TagSerializer(ValidatedModelSerializer):
  204. url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
  205. object_types = ContentTypeField(
  206. queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
  207. many=True,
  208. required=False
  209. )
  210. tagged_items = serializers.IntegerField(read_only=True)
  211. class Meta:
  212. model = Tag
  213. fields = [
  214. 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
  215. 'last_updated',
  216. ]
  217. #
  218. # Image attachments
  219. #
  220. class ImageAttachmentSerializer(ValidatedModelSerializer):
  221. url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
  222. content_type = ContentTypeField(
  223. queryset=ContentType.objects.all()
  224. )
  225. parent = serializers.SerializerMethodField(read_only=True)
  226. class Meta:
  227. model = ImageAttachment
  228. fields = [
  229. 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
  230. 'image_width', 'created', 'last_updated',
  231. ]
  232. def validate(self, data):
  233. # Validate that the parent object exists
  234. try:
  235. data['content_type'].get_object_for_this_type(id=data['object_id'])
  236. except ObjectDoesNotExist:
  237. raise serializers.ValidationError(
  238. "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
  239. )
  240. # Enforce model validation
  241. super().validate(data)
  242. return data
  243. @extend_schema_field(serializers.JSONField(allow_null=True))
  244. def get_parent(self, obj):
  245. serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
  246. return serializer(obj.parent, context={'request': self.context['request']}).data
  247. #
  248. # Journal entries
  249. #
  250. class JournalEntrySerializer(NetBoxModelSerializer):
  251. url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
  252. assigned_object_type = ContentTypeField(
  253. queryset=ContentType.objects.all()
  254. )
  255. assigned_object = serializers.SerializerMethodField(read_only=True)
  256. created_by = serializers.PrimaryKeyRelatedField(
  257. allow_null=True,
  258. queryset=get_user_model().objects.all(),
  259. required=False,
  260. default=serializers.CurrentUserDefault()
  261. )
  262. kind = ChoiceField(
  263. choices=JournalEntryKindChoices,
  264. required=False
  265. )
  266. class Meta:
  267. model = JournalEntry
  268. fields = [
  269. 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
  270. 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
  271. ]
  272. def validate(self, data):
  273. # Validate that the parent object exists
  274. if 'assigned_object_type' in data and 'assigned_object_id' in data:
  275. try:
  276. data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
  277. except ObjectDoesNotExist:
  278. raise serializers.ValidationError(
  279. f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
  280. )
  281. # Enforce model validation
  282. super().validate(data)
  283. return data
  284. @extend_schema_field(serializers.JSONField(allow_null=True))
  285. def get_assigned_object(self, instance):
  286. serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
  287. context = {'request': self.context['request']}
  288. return serializer(instance.assigned_object, context=context).data
  289. #
  290. # Config contexts
  291. #
  292. class ConfigContextSerializer(ValidatedModelSerializer):
  293. url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
  294. regions = SerializedPKRelatedField(
  295. queryset=Region.objects.all(),
  296. serializer=NestedRegionSerializer,
  297. required=False,
  298. many=True
  299. )
  300. site_groups = SerializedPKRelatedField(
  301. queryset=SiteGroup.objects.all(),
  302. serializer=NestedSiteGroupSerializer,
  303. required=False,
  304. many=True
  305. )
  306. sites = SerializedPKRelatedField(
  307. queryset=Site.objects.all(),
  308. serializer=NestedSiteSerializer,
  309. required=False,
  310. many=True
  311. )
  312. locations = SerializedPKRelatedField(
  313. queryset=Location.objects.all(),
  314. serializer=NestedLocationSerializer,
  315. required=False,
  316. many=True
  317. )
  318. device_types = SerializedPKRelatedField(
  319. queryset=DeviceType.objects.all(),
  320. serializer=NestedDeviceTypeSerializer,
  321. required=False,
  322. many=True
  323. )
  324. roles = SerializedPKRelatedField(
  325. queryset=DeviceRole.objects.all(),
  326. serializer=NestedDeviceRoleSerializer,
  327. required=False,
  328. many=True
  329. )
  330. platforms = SerializedPKRelatedField(
  331. queryset=Platform.objects.all(),
  332. serializer=NestedPlatformSerializer,
  333. required=False,
  334. many=True
  335. )
  336. cluster_types = SerializedPKRelatedField(
  337. queryset=ClusterType.objects.all(),
  338. serializer=NestedClusterTypeSerializer,
  339. required=False,
  340. many=True
  341. )
  342. cluster_groups = SerializedPKRelatedField(
  343. queryset=ClusterGroup.objects.all(),
  344. serializer=NestedClusterGroupSerializer,
  345. required=False,
  346. many=True
  347. )
  348. clusters = SerializedPKRelatedField(
  349. queryset=Cluster.objects.all(),
  350. serializer=NestedClusterSerializer,
  351. required=False,
  352. many=True
  353. )
  354. tenant_groups = SerializedPKRelatedField(
  355. queryset=TenantGroup.objects.all(),
  356. serializer=NestedTenantGroupSerializer,
  357. required=False,
  358. many=True
  359. )
  360. tenants = SerializedPKRelatedField(
  361. queryset=Tenant.objects.all(),
  362. serializer=NestedTenantSerializer,
  363. required=False,
  364. many=True
  365. )
  366. tags = serializers.SlugRelatedField(
  367. queryset=Tag.objects.all(),
  368. slug_field='slug',
  369. required=False,
  370. many=True
  371. )
  372. data_source = NestedDataSourceSerializer(
  373. required=False
  374. )
  375. data_file = NestedDataFileSerializer(
  376. read_only=True
  377. )
  378. class Meta:
  379. model = ConfigContext
  380. fields = [
  381. 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
  382. 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
  383. 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
  384. 'created', 'last_updated',
  385. ]
  386. #
  387. # Config templates
  388. #
  389. class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
  390. url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
  391. data_source = NestedDataSourceSerializer(
  392. required=False
  393. )
  394. data_file = NestedDataFileSerializer(
  395. read_only=True
  396. )
  397. class Meta:
  398. model = ConfigTemplate
  399. fields = [
  400. 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
  401. 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
  402. ]
  403. #
  404. # Reports
  405. #
  406. class ReportSerializer(serializers.Serializer):
  407. url = serializers.HyperlinkedIdentityField(
  408. view_name='extras-api:report-detail',
  409. lookup_field='full_name',
  410. lookup_url_kwarg='pk'
  411. )
  412. id = serializers.CharField(read_only=True, source="full_name")
  413. module = serializers.CharField(max_length=255)
  414. name = serializers.CharField(max_length=255)
  415. description = serializers.CharField(max_length=255, required=False)
  416. test_methods = serializers.ListField(child=serializers.CharField(max_length=255))
  417. result = NestedJobSerializer()
  418. display = serializers.SerializerMethodField(read_only=True)
  419. @extend_schema_field(serializers.CharField())
  420. def get_display(self, obj):
  421. return f'{obj.name} ({obj.module})'
  422. class ReportDetailSerializer(ReportSerializer):
  423. result = JobSerializer()
  424. class ReportInputSerializer(serializers.Serializer):
  425. schedule_at = serializers.DateTimeField(required=False, allow_null=True)
  426. interval = serializers.IntegerField(required=False, allow_null=True)
  427. def validate_schedule_at(self, value):
  428. if value and not self.context['report'].scheduling_enabled:
  429. raise serializers.ValidationError("Scheduling is not enabled for this report.")
  430. return value
  431. def validate_interval(self, value):
  432. if value and not self.context['report'].scheduling_enabled:
  433. raise serializers.ValidationError("Scheduling is not enabled for this report.")
  434. return value
  435. #
  436. # Scripts
  437. #
  438. class ScriptSerializer(serializers.Serializer):
  439. url = serializers.HyperlinkedIdentityField(
  440. view_name='extras-api:script-detail',
  441. lookup_field='full_name',
  442. lookup_url_kwarg='pk'
  443. )
  444. id = serializers.CharField(read_only=True, source="full_name")
  445. module = serializers.CharField(max_length=255)
  446. name = serializers.CharField(read_only=True)
  447. description = serializers.CharField(read_only=True)
  448. vars = serializers.SerializerMethodField(read_only=True)
  449. result = NestedJobSerializer()
  450. display = serializers.SerializerMethodField(read_only=True)
  451. @extend_schema_field(serializers.JSONField(allow_null=True))
  452. def get_vars(self, instance):
  453. return {
  454. k: v.__class__.__name__ for k, v in instance._get_vars().items()
  455. }
  456. @extend_schema_field(serializers.CharField())
  457. def get_display(self, obj):
  458. return f'{obj.name} ({obj.module})'
  459. class ScriptDetailSerializer(ScriptSerializer):
  460. result = JobSerializer()
  461. class ScriptInputSerializer(serializers.Serializer):
  462. data = serializers.JSONField()
  463. commit = serializers.BooleanField()
  464. schedule_at = serializers.DateTimeField(required=False, allow_null=True)
  465. interval = serializers.IntegerField(required=False, allow_null=True)
  466. def validate_schedule_at(self, value):
  467. if value and not self.context['script'].scheduling_enabled:
  468. raise serializers.ValidationError("Scheduling is not enabled for this script.")
  469. return value
  470. def validate_interval(self, value):
  471. if value and not self.context['script'].scheduling_enabled:
  472. raise serializers.ValidationError("Scheduling is not enabled for this script.")
  473. return value
  474. class ScriptLogMessageSerializer(serializers.Serializer):
  475. status = serializers.SerializerMethodField(read_only=True)
  476. message = serializers.SerializerMethodField(read_only=True)
  477. def get_status(self, instance):
  478. return instance[0]
  479. def get_message(self, instance):
  480. return instance[1]
  481. class ScriptOutputSerializer(serializers.Serializer):
  482. log = ScriptLogMessageSerializer(many=True, read_only=True)
  483. output = serializers.CharField(read_only=True)
  484. #
  485. # Change logging
  486. #
  487. class ObjectChangeSerializer(BaseModelSerializer):
  488. url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
  489. user = NestedUserSerializer(
  490. read_only=True
  491. )
  492. action = ChoiceField(
  493. choices=ObjectChangeActionChoices,
  494. read_only=True
  495. )
  496. changed_object_type = ContentTypeField(
  497. read_only=True
  498. )
  499. changed_object = serializers.SerializerMethodField(
  500. read_only=True
  501. )
  502. class Meta:
  503. model = ObjectChange
  504. fields = [
  505. 'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
  506. 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
  507. ]
  508. @extend_schema_field(serializers.JSONField(allow_null=True))
  509. def get_changed_object(self, obj):
  510. """
  511. Serialize a nested representation of the changed object.
  512. """
  513. if obj.changed_object is None:
  514. return None
  515. try:
  516. serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX)
  517. except SerializerNotFound:
  518. return obj.object_repr
  519. context = {
  520. 'request': self.context['request']
  521. }
  522. data = serializer(obj.changed_object, context=context).data
  523. return data
  524. #
  525. # ContentTypes
  526. #
  527. class ContentTypeSerializer(BaseModelSerializer):
  528. url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
  529. class Meta:
  530. model = ContentType
  531. fields = ['id', 'url', 'display', 'app_label', 'model']
  532. #
  533. # User dashboard
  534. #
  535. class DashboardSerializer(serializers.ModelSerializer):
  536. class Meta:
  537. model = Dashboard
  538. fields = ('layout', 'config')