devices.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import decimal
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.utils.translation import gettext as _
  4. from drf_spectacular.utils import extend_schema_field
  5. from rest_framework import serializers
  6. from dcim.choices import *
  7. from dcim.constants import MACADDRESS_ASSIGNMENT_MODELS, MODULE_TOKEN
  8. from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceContext
  9. from dcim.utils import get_module_bay_positions, resolve_module_placeholder
  10. from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
  11. from ipam.api.serializers_.ip import IPAddressSerializer
  12. from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
  13. from netbox.api.gfk_fields import GFKSerializerField
  14. from netbox.api.serializers import PrimaryModelSerializer
  15. from tenancy.api.serializers_.tenants import TenantSerializer
  16. from virtualization.api.serializers_.clusters import ClusterSerializer
  17. from .devicetypes import *
  18. from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer
  19. from .platforms import PlatformSerializer
  20. from .racks import RackSerializer
  21. from .roles import DeviceRoleSerializer
  22. from .sites import LocationSerializer, SiteSerializer
  23. from .virtualchassis import VirtualChassisSerializer
  24. __all__ = (
  25. 'DeviceSerializer',
  26. 'MACAddressSerializer',
  27. 'ModuleSerializer',
  28. 'VirtualDeviceContextSerializer',
  29. )
  30. class DeviceSerializer(PrimaryModelSerializer):
  31. device_type = DeviceTypeSerializer(nested=True)
  32. role = DeviceRoleSerializer(nested=True)
  33. tenant = TenantSerializer(
  34. nested=True,
  35. required=False,
  36. allow_null=True,
  37. default=None
  38. )
  39. platform = PlatformSerializer(nested=True, required=False, allow_null=True)
  40. site = SiteSerializer(nested=True)
  41. location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
  42. rack = RackSerializer(nested=True, required=False, allow_null=True, default=None)
  43. face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
  44. position = serializers.DecimalField(
  45. max_digits=4,
  46. decimal_places=1,
  47. allow_null=True,
  48. label=_('Position (U)'),
  49. min_value=decimal.Decimal(0.5),
  50. default=None
  51. )
  52. status = ChoiceField(choices=DeviceStatusChoices, required=False)
  53. airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
  54. primary_ip = IPAddressSerializer(
  55. nested=True,
  56. read_only=True,
  57. allow_null=True,
  58. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  59. )
  60. primary_ip4 = IPAddressSerializer(
  61. nested=True,
  62. required=False,
  63. allow_null=True,
  64. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  65. )
  66. primary_ip6 = IPAddressSerializer(
  67. nested=True,
  68. required=False,
  69. allow_null=True,
  70. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  71. )
  72. oob_ip = IPAddressSerializer(
  73. nested=True,
  74. required=False,
  75. allow_null=True,
  76. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  77. )
  78. parent_device = serializers.SerializerMethodField()
  79. cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
  80. virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None)
  81. vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
  82. config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
  83. config_context = serializers.SerializerMethodField(read_only=True)
  84. # Counter fields
  85. console_port_count = serializers.IntegerField(read_only=True)
  86. console_server_port_count = serializers.IntegerField(read_only=True)
  87. power_port_count = serializers.IntegerField(read_only=True)
  88. power_outlet_count = serializers.IntegerField(read_only=True)
  89. interface_count = serializers.IntegerField(read_only=True)
  90. front_port_count = serializers.IntegerField(read_only=True)
  91. rear_port_count = serializers.IntegerField(read_only=True)
  92. device_bay_count = serializers.IntegerField(read_only=True)
  93. module_bay_count = serializers.IntegerField(read_only=True)
  94. inventory_item_count = serializers.IntegerField(read_only=True)
  95. class Meta:
  96. model = Device
  97. fields = [
  98. 'id', 'url', 'display_url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial',
  99. 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
  100. 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
  101. 'vc_position', 'vc_priority', 'description', 'owner', 'comments', 'config_template', 'config_context',
  102. 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
  103. 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
  104. 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
  105. ]
  106. brief_fields = ('id', 'url', 'display', 'name', 'description')
  107. @extend_schema_field(NestedDeviceSerializer(allow_null=True))
  108. def get_parent_device(self, obj):
  109. try:
  110. device_bay = obj.parent_bay
  111. except DeviceBay.DoesNotExist:
  112. return None
  113. context = {'request': self.context['request']}
  114. data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
  115. data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
  116. return data
  117. @extend_schema_field(serializers.JSONField(allow_null=True))
  118. def get_config_context(self, obj):
  119. return obj.get_config_context()
  120. class VirtualDeviceContextSerializer(PrimaryModelSerializer):
  121. device = DeviceSerializer(nested=True)
  122. identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None)
  123. tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
  124. primary_ip = IPAddressSerializer(
  125. nested=True,
  126. read_only=True,
  127. allow_null=True,
  128. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  129. )
  130. primary_ip4 = IPAddressSerializer(
  131. nested=True,
  132. required=False,
  133. allow_null=True,
  134. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  135. )
  136. primary_ip6 = IPAddressSerializer(
  137. nested=True,
  138. required=False,
  139. allow_null=True,
  140. fields=[*IPAddressSerializer.Meta.brief_fields, 'dns_name', 'nat_inside', 'nat_outside'],
  141. )
  142. status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
  143. # Related object counts
  144. interface_count = RelatedObjectCountField('interfaces')
  145. class Meta:
  146. model = VirtualDeviceContext
  147. fields = [
  148. 'id', 'url', 'display_url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip',
  149. 'primary_ip4', 'primary_ip6', 'status', 'description', 'owner', 'comments', 'tags', 'custom_fields',
  150. 'created', 'last_updated', 'interface_count',
  151. ]
  152. brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
  153. class ModuleSerializer(PrimaryModelSerializer):
  154. device = DeviceSerializer(nested=True)
  155. module_bay = NestedModuleBaySerializer()
  156. module_type = ModuleTypeSerializer(nested=True)
  157. status = ChoiceField(choices=ModuleStatusChoices, required=False)
  158. replicate_components = serializers.BooleanField(
  159. required=False,
  160. default=True,
  161. write_only=True,
  162. label=_('Replicate components'),
  163. help_text=_('Automatically populate components associated with this module type (default: true)')
  164. )
  165. adopt_components = serializers.BooleanField(
  166. required=False,
  167. default=False,
  168. write_only=True,
  169. label=_('Adopt components'),
  170. help_text=_('Adopt already existing components')
  171. )
  172. is_bay_compatible = serializers.BooleanField(read_only=True)
  173. class Meta:
  174. model = Module
  175. fields = [
  176. 'id', 'url', 'display_url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial',
  177. 'asset_tag', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
  178. 'replicate_components', 'adopt_components', 'is_bay_compatible',
  179. ]
  180. brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
  181. def validate(self, data):
  182. # When used as a nested serializer (e.g. as the `module` field on device component
  183. # serializers), `data` is already a resolved Module instance — skip our custom logic.
  184. if self.nested:
  185. return super().validate(data)
  186. # Pop write-only transient fields before ValidatedModelSerializer tries to
  187. # construct a Module instance for full_clean(); restore them afterwards.
  188. replicate_components = data.pop('replicate_components', True)
  189. adopt_components = data.pop('adopt_components', False)
  190. if self.instance is not None:
  191. # Derive device from module_bay so full_clean() validates a consistent pair.
  192. if 'module_bay' in data and 'device' not in data:
  193. data['device'] = data['module_bay'].device
  194. move_requested = (
  195. ('module_bay' in data and data['module_bay'].pk != self.instance.module_bay_id) or
  196. ('device' in data and data['device'].pk != self.instance.device_id)
  197. )
  198. if move_requested and 'module_type' in data and data['module_type'].pk != self.instance.module_type_id:
  199. raise serializers.ValidationError({
  200. 'module_type': _(
  201. "Changing a module's type while moving it is not supported. Change the module "
  202. "type and move the module as separate operations."
  203. )
  204. })
  205. data = super().validate(data)
  206. # For updates these fields are not meaningful; omit them from validated_data so that
  207. # ModelSerializer.update() does not set unexpected attributes on the instance.
  208. if self.instance:
  209. return data
  210. # Always pass the flags to create() so it can set the correct private attributes.
  211. data['replicate_components'] = replicate_components
  212. data['adopt_components'] = adopt_components
  213. # Skip conflict checks when no component operations are requested.
  214. if not replicate_components and not adopt_components:
  215. return data
  216. device = data.get('device')
  217. module_type = data.get('module_type')
  218. module_bay = data.get('module_bay')
  219. # Required-field validation fires separately; skip here if any are missing.
  220. if not all([device, module_type, module_bay]):
  221. return data
  222. try:
  223. positions = get_module_bay_positions(module_bay)
  224. except ValueError as e:
  225. raise serializers.ValidationError({'module_bay': str(e)}) from e
  226. for templates_attr, component_attr in [
  227. ('consoleporttemplates', 'consoleports'),
  228. ('consoleserverporttemplates', 'consoleserverports'),
  229. ('interfacetemplates', 'interfaces'),
  230. ('powerporttemplates', 'powerports'),
  231. ('poweroutlettemplates', 'poweroutlets'),
  232. ('rearporttemplates', 'rearports'),
  233. ('frontporttemplates', 'frontports'),
  234. ]:
  235. installed_components = {
  236. component.name: component
  237. for component in getattr(device, component_attr).all()
  238. }
  239. for template in getattr(module_type, templates_attr).all():
  240. resolved_name = template.name
  241. if MODULE_TOKEN in template.name:
  242. if not module_bay.position:
  243. raise serializers.ValidationError(
  244. _("Cannot install module with placeholder values in a module bay with no position defined.")
  245. )
  246. try:
  247. resolved_name = resolve_module_placeholder(template.name, positions)
  248. except ValueError as e:
  249. raise serializers.ValidationError(str(e))
  250. existing_item = installed_components.get(resolved_name)
  251. if adopt_components and existing_item and existing_item.module:
  252. raise serializers.ValidationError(
  253. _("Cannot adopt {model} {name} as it already belongs to a module").format(
  254. model=template.component_model.__name__,
  255. name=resolved_name
  256. )
  257. )
  258. if not adopt_components and resolved_name in installed_components:
  259. raise serializers.ValidationError(
  260. _("A {model} named {name} already exists").format(
  261. model=template.component_model.__name__,
  262. name=resolved_name
  263. )
  264. )
  265. return data
  266. def create(self, validated_data):
  267. replicate_components = validated_data.pop('replicate_components', True)
  268. adopt_components = validated_data.pop('adopt_components', False)
  269. # Tags are handled after save; pop them here to pass to _save_tags()
  270. tags = validated_data.pop('tags', None)
  271. # _adopt_components and _disable_replication must be set on the instance before
  272. # save() is called, so we cannot delegate to super().create() here.
  273. instance = self.Meta.model(**validated_data)
  274. if adopt_components:
  275. instance._adopt_components = True
  276. if not replicate_components:
  277. instance._disable_replication = True
  278. instance.save()
  279. if tags is not None:
  280. self._save_tags(instance, tags)
  281. return instance
  282. class MACAddressSerializer(PrimaryModelSerializer):
  283. assigned_object_type = ContentTypeField(
  284. queryset=ContentType.objects.filter(MACADDRESS_ASSIGNMENT_MODELS),
  285. required=False,
  286. allow_null=True
  287. )
  288. assigned_object = GFKSerializerField(read_only=True)
  289. is_primary = serializers.BooleanField(read_only=True)
  290. class Meta:
  291. model = MACAddress
  292. fields = [
  293. 'id', 'url', 'display_url', 'display', 'mac_address', 'assigned_object_type', 'assigned_object_id',
  294. 'assigned_object', 'is_primary', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created',
  295. 'last_updated',
  296. ]
  297. brief_fields = ('id', 'url', 'display', 'mac_address', 'description')