5
5
import logging
6
6
from dataclasses import dataclass
7
7
from functools import cache , lru_cache
8
- from typing import Type
9
8
10
9
import netaddr
11
10
from django .contrib .contenttypes .fields import ContentType
18
17
from extras .models .customfields import CustomField
19
18
20
19
from .common import UnresolvedReference
20
+ from .compat import in_version_range
21
21
from .plugin_utils import content_type_id , get_object_type , get_object_type_model
22
22
23
23
logger = logging .getLogger (__name__ )
163
163
name = "logical_service_name_no_device_or_vm" ,
164
164
model_class = get_object_type_model ("ipam.service" ),
165
165
condition = Q (device__isnull = True , virtual_machine__isnull = True ),
166
+ max_version = "4.2.99" ,
166
167
),
167
168
ObjectMatchCriteria (
168
169
fields = ("name" , "device" ),
169
170
name = "logical_service_name_on_device" ,
170
171
model_class = get_object_type_model ("ipam.service" ),
171
172
condition = Q (device__isnull = False ),
173
+ max_version = "4.2.99" ,
172
174
),
173
175
ObjectMatchCriteria (
174
176
fields = ("name" , "virtual_machine" ),
175
177
name = "logical_service_name_on_vm" ,
176
178
model_class = get_object_type_model ("ipam.service" ),
177
179
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"
178
188
),
179
189
],
180
190
"dcim.modulebay" : lambda : [
202
212
model_class = get_object_type_model ("ipam.fhrpgroup" ),
203
213
)
204
214
],
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
+ ],
205
241
}
206
242
207
243
@dataclass
@@ -221,9 +257,12 @@ class ObjectMatchCriteria:
221
257
fields : tuple [str ] | None = None
222
258
expressions : tuple | None = None
223
259
condition : Q | None = None
224
- model_class : Type [models .Model ] | None = None
260
+ model_class : type [models .Model ] | None = None
225
261
name : str | None = None
226
262
263
+ min_version : str | None = None
264
+ max_version : str | None = None
265
+
227
266
def __hash__ (self ):
228
267
"""Hash the object match criteria."""
229
268
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:
365
404
"""Builds a queryset for the constraint with the given data."""
366
405
data = self ._prepare_data (data )
367
406
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
369
408
for field , value in data .items ()
370
409
}
371
410
@@ -413,7 +452,10 @@ class CustomFieldMatcher:
413
452
414
453
name : str
415
454
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
417
459
418
460
def fingerprint (self , data : dict ) -> str | None :
419
461
"""Fingerprint the custom field value."""
@@ -448,9 +490,12 @@ class GlobalIPNetworkIPMatcher:
448
490
449
491
ip_fields : tuple [str ]
450
492
vrf_field : str
451
- model_class : Type [models .Model ]
493
+ model_class : type [models .Model ]
452
494
name : str
453
495
496
+ min_version : str | None = None
497
+ max_version : str | None = None
498
+
454
499
def _check_condition (self , data : dict ) -> bool :
455
500
"""Check the condition for the custom field."""
456
501
return data .get (self .vrf_field , None ) is None
@@ -508,9 +553,12 @@ class VRFIPNetworkIPMatcher:
508
553
509
554
ip_fields : tuple [str ]
510
555
vrf_field : str
511
- model_class : Type [models .Model ]
556
+ model_class : type [models .Model ]
512
557
name : str
513
558
559
+ min_version : str | None = None
560
+ max_version : str | None = None
561
+
514
562
def _check_condition (self , data : dict ) -> bool :
515
563
"""Check the condition for the custom field."""
516
564
return data .get (self .vrf_field , None ) is not None
@@ -583,7 +631,10 @@ class AutoSlugMatcher:
583
631
584
632
name : str
585
633
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
587
638
588
639
def fingerprint (self , data : dict ) -> str | None :
589
640
"""Fingerprint the custom field value."""
@@ -650,7 +701,10 @@ def _get_autoslug_matchers(model_class) -> list:
650
701
@lru_cache (maxsize = 256 )
651
702
def _get_model_matchers (model_class ) -> list [ObjectMatchCriteria ]:
652
703
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
+ ]
654
708
655
709
# collect single fields that are unique
656
710
for field in model_class ._meta .fields :
@@ -750,7 +804,7 @@ def _fingerprint_all(data: dict, object_type: str|None = None) -> str:
750
804
if k .startswith ("_" ):
751
805
continue
752
806
values .append (k )
753
- if isinstance (v , ( list , tuple ) ):
807
+ if isinstance (v , list | tuple ):
754
808
values .extend (sorted (v ))
755
809
elif isinstance (v , dict ):
756
810
values .append (_fingerprint_all (v ))
0 commit comments