55import logging
66from dataclasses import dataclass
77from functools import cache , lru_cache
8- from typing import Type
98
109import netaddr
1110from django .contrib .contenttypes .fields import ContentType
1817from extras .models .customfields import CustomField
1918
2019from .common import UnresolvedReference
20+ from .compat import in_version_range
2121from .plugin_utils import content_type_id , get_object_type , get_object_type_model
2222
2323logger = logging .getLogger (__name__ )
163163 name = "logical_service_name_no_device_or_vm" ,
164164 model_class = get_object_type_model ("ipam.service" ),
165165 condition = Q (device__isnull = True , virtual_machine__isnull = True ),
166+ max_version = "4.2.99" ,
166167 ),
167168 ObjectMatchCriteria (
168169 fields = ("name" , "device" ),
169170 name = "logical_service_name_on_device" ,
170171 model_class = get_object_type_model ("ipam.service" ),
171172 condition = Q (device__isnull = False ),
173+ max_version = "4.2.99" ,
172174 ),
173175 ObjectMatchCriteria (
174176 fields = ("name" , "virtual_machine" ),
175177 name = "logical_service_name_on_vm" ,
176178 model_class = get_object_type_model ("ipam.service" ),
177179 condition = Q (virtual_machine__isnull = False ),
180+ max_version = "4.2.99" ,
181+ ),
182+ ObjectMatchCriteria (
183+ fields = ("name" , "parent_object_type" , "parent_object_id" ),
184+ name = "logical_service_name_on_parent" ,
185+ model_class = get_object_type_model ("ipam.service" ),
186+ condition = Q (parent_object_type__isnull = False ),
187+ min_version = "4.3.0"
178188 ),
179189 ],
180190 "dcim.modulebay" : lambda : [
202212 model_class = get_object_type_model ("ipam.fhrpgroup" ),
203213 )
204214 ],
215+ "tenancy.contact" : lambda : [
216+ ObjectMatchCriteria (
217+ # contacts are unconstrained in 4.3.0
218+ # in 4.2 they are constrained by unique name per group
219+ fields = ("name" , ),
220+ name = "logical_contact_name" ,
221+ model_class = get_object_type_model ("tenancy.contact" ),
222+ min_version = "4.3.0" ,
223+ )
224+ ],
225+ "dcim.devicerole" : lambda : [
226+ ObjectMatchCriteria (
227+ fields = ("name" ,),
228+ name = "logical_device_role_name_no_parent" ,
229+ model_class = get_object_type_model ("dcim.devicerole" ),
230+ condition = Q (parent__isnull = True ),
231+ min_version = "4.3.0" ,
232+ ),
233+ ObjectMatchCriteria (
234+ fields = ("slug" ,),
235+ name = "logical_device_role_slug_no_parent" ,
236+ model_class = get_object_type_model ("dcim.devicerole" ),
237+ condition = Q (parent__isnull = True ),
238+ min_version = "4.3.0" ,
239+ )
240+ ],
205241}
206242
207243@dataclass
@@ -221,9 +257,12 @@ class ObjectMatchCriteria:
221257 fields : tuple [str ] | None = None
222258 expressions : tuple | None = None
223259 condition : Q | None = None
224- model_class : Type [models .Model ] | None = None
260+ model_class : type [models .Model ] | None = None
225261 name : str | None = None
226262
263+ min_version : str | None = None
264+ max_version : str | None = None
265+
227266 def __hash__ (self ):
228267 """Hash the object match criteria."""
229268 return hash ((self .fields , self .expressions , self .condition , self .model_class .__name__ , self .name ))
@@ -365,7 +404,7 @@ def _build_expressions_queryset(self, data) -> models.QuerySet:
365404 """Builds a queryset for the constraint with the given data."""
366405 data = self ._prepare_data (data )
367406 replacements = {
368- F (field ): Value (value ) if isinstance (value , ( str , int , float , bool ) ) else value
407+ F (field ): Value (value ) if isinstance (value , str | int | float | bool ) else value
369408 for field , value in data .items ()
370409 }
371410
@@ -413,7 +452,10 @@ class CustomFieldMatcher:
413452
414453 name : str
415454 custom_field : str
416- model_class : Type [models .Model ]
455+ model_class : type [models .Model ]
456+
457+ min_version : str | None = None
458+ max_version : str | None = None
417459
418460 def fingerprint (self , data : dict ) -> str | None :
419461 """Fingerprint the custom field value."""
@@ -448,9 +490,12 @@ class GlobalIPNetworkIPMatcher:
448490
449491 ip_fields : tuple [str ]
450492 vrf_field : str
451- model_class : Type [models .Model ]
493+ model_class : type [models .Model ]
452494 name : str
453495
496+ min_version : str | None = None
497+ max_version : str | None = None
498+
454499 def _check_condition (self , data : dict ) -> bool :
455500 """Check the condition for the custom field."""
456501 return data .get (self .vrf_field , None ) is None
@@ -508,9 +553,12 @@ class VRFIPNetworkIPMatcher:
508553
509554 ip_fields : tuple [str ]
510555 vrf_field : str
511- model_class : Type [models .Model ]
556+ model_class : type [models .Model ]
512557 name : str
513558
559+ min_version : str | None = None
560+ max_version : str | None = None
561+
514562 def _check_condition (self , data : dict ) -> bool :
515563 """Check the condition for the custom field."""
516564 return data .get (self .vrf_field , None ) is not None
@@ -583,7 +631,10 @@ class AutoSlugMatcher:
583631
584632 name : str
585633 slug_field : str
586- model_class : Type [models .Model ]
634+ model_class : type [models .Model ]
635+
636+ min_version : str | None = None
637+ max_version : str | None = None
587638
588639 def fingerprint (self , data : dict ) -> str | None :
589640 """Fingerprint the custom field value."""
@@ -650,7 +701,10 @@ def _get_autoslug_matchers(model_class) -> list:
650701@lru_cache (maxsize = 256 )
651702def _get_model_matchers (model_class ) -> list [ObjectMatchCriteria ]:
652703 object_type = get_object_type (model_class )
653- matchers = _LOGICAL_MATCHERS .get (object_type , lambda : [])()
704+ matchers = [
705+ x for x in _LOGICAL_MATCHERS .get (object_type , lambda : [])()
706+ if in_version_range (x .min_version , x .max_version )
707+ ]
654708
655709 # collect single fields that are unique
656710 for field in model_class ._meta .fields :
@@ -750,7 +804,7 @@ def _fingerprint_all(data: dict, object_type: str|None = None) -> str:
750804 if k .startswith ("_" ):
751805 continue
752806 values .append (k )
753- if isinstance (v , ( list , tuple ) ):
807+ if isinstance (v , list | tuple ):
754808 values .extend (sorted (v ))
755809 elif isinstance (v , dict ):
756810 values .append (_fingerprint_all (v ))
0 commit comments