@@ -45,29 +45,32 @@ def getPoliciesThatApply(decisionParams):
4545
4646 # Get policies that match the given decisionParameters
4747 for policyName , policySetup in policiesConfig .items ():
48- # The parameter policyType replaces policyName, so if it is not present,
49- # we pick policyName
48+ # The parameter policyType is mandatory. If not present, skip this entry —
49+ # it is a command-args defaults section, not a policy definition.
5050 try :
5151 policyType = policySetup ["policyType" ][0 ]
5252 except KeyError :
53- policyType = policyName
54- # continue
53+ continue
5554
5655 # The section matchParams is not mandatory, so we set {} as default.
5756 policyMatchParams = policySetup .get ("matchParams" , {})
5857 gLogger .debug (f"matchParams of { policyName } : { str (policyMatchParams )} " )
5958
60- # FIXME: make sure the values in the policyConfigParams dictionary are typed !!
61- policyConfigParams = {}
62- # policyConfigParams = policySetup.get( 'configParams', {} )
59+ # Any key in the CS policy entry that is not a reserved keyword is treated as
60+ # a command-argument override. These override the defaults from POLICIESMETA.
61+ _reservedKeys = {"policyType" , "matchParams" , "configParams" , "doNotCombineResult" , "active" }
62+ policyConfigParams = {
63+ k : v [0 ] if isinstance (v , list ) else v for k , v in policySetup .items () if k not in _reservedKeys
64+ }
65+
6366 policyMatch = Utils .configMatch (decisionParams , policyMatchParams )
6467 gLogger .debug (f"PolicyMatch for decisionParams { decisionParams } : { str (policyMatch )} " )
6568
6669 # WARNING: we need an additional filtering function when the matching
6770 # is not straightforward (e.g. when the policy specify a 'domain', while
6871 # the decisionParams has only the name of the element)
6972 if policyMatch and _filterPolicies (decisionParams , policyMatchParams ):
70- policiesThatApply .append ((policyName , policyType , policyConfigParams ))
73+ policiesThatApply .append ((policyName , policyType , policyConfigParams , policyMatchParams ))
7174
7275 gLogger .debug (f"policies that apply (before post-processing): { str (policiesThatApply )} " )
7376 policiesThatApply = postProcessingPolicyList (policiesThatApply )
@@ -76,7 +79,7 @@ def getPoliciesThatApply(decisionParams):
7679 objectLoader = ObjectLoader ()
7780 policiesToBeLoaded = []
7881 # Gets policies parameters from code.
79- for policyName , policyType , _policyConfigParams in policiesThatApply :
82+ for policyName , policyType , _policyConfigParams , _policyMatchParams in policiesThatApply :
8083 try :
8184 result = objectLoader .loadModule ("DIRAC.ResourceStatusSystem.Policy.Configurations" )
8285 if not result ["OK" ]:
@@ -92,8 +95,22 @@ def getPoliciesThatApply(decisionParams):
9295 policyDict = {"name" : policyName , "type" : policyType , "args" : {}}
9396
9497 # args is one of the parameters we are going to use on the policies. We copy
95- # the defaults and then we update if with whatever comes from the CS.
98+ # the defaults from POLICIESMETA and then override with whatever comes from the CS.
9699 policyDict .update (policyMeta )
100+ if _policyConfigParams and policyDict .get ("args" ) is not None :
101+ # Build a case-insensitive lookup of the existing arg keys so that CS keys
102+ # like "Unit" correctly override POLICIESMETA keys like "unit".
103+ argsKeyMap = {k .lower (): k for k in policyDict ["args" ]}
104+ for csKey , csVal in _policyConfigParams .items ():
105+ targetKey = argsKeyMap .get (csKey .lower (), csKey )
106+ # CS values are always strings; cast to the type of the existing default.
107+ existingVal = policyDict ["args" ].get (targetKey )
108+ if existingVal is not None :
109+ try :
110+ csVal = type (existingVal )(csVal )
111+ except (ValueError , TypeError ):
112+ pass
113+ policyDict ["args" ][targetKey ] = csVal
97114
98115 policiesToBeLoaded .append (policyDict )
99116
@@ -262,28 +279,42 @@ def _filterPolicies(decisionParams, policyMatchParams):
262279
263280
264281def postProcessingPolicyList (policiesThatApply ):
265- """Put here any "hacky" post-processing"""
266-
267- # FIXME: the following 2 "if" are a "hack" for dealing with the following case:
268- # an SE happens to be subject to, e.g., both the 'FreeDiskSpaceMB' and the 'FreeDiskSpaceGB' policies
269- # (currently, there is no way to avoid that this happens, see e.g. LogSE)
270- # When this is the case, supposing that an SE has 50 MB free, the policies evaluation will be the following:
271- # - 'FreeDiskSpaceMB' will evaluate 'Active'
272- # - 'FreeDiskSpaceGB' will evaluate 'Banned'
273- # so the SE will end up being banned, but we want only the 'FreeDiskSpaceMB' to be considered .
274- if ( "FreeDiskSpaceMB" , "FreeDiskSpaceMB" , {}) in policiesThatApply :
275- try :
276- policiesThatApply . remove (( "FreeDiskSpaceGB" , "FreeDiskSpaceGB" , {}))
277- except ValueError :
278- pass
279- try :
280- policiesThatApply . remove (( "FreeDiskSpaceTB" , "FreeDiskSpaceTB" , {}))
281- except ValueError :
282- pass
283- if ( "FreeDiskSpaceGB" , "FreeDiskSpaceGB" , {}) in policiesThatApply :
284- try :
285- policiesThatApply . remove (( "FreeDiskSpaceTB" , "FreeDiskSpaceTB" , {}))
286- except ValueError :
287- pass
282+ """Remove lower-priority duplicates when multiple policies of the same type apply.
283+
284+ When two or more policies share the same ``policyType`` and both match the current
285+ element, we keep only the most specific one. Specificity is determined by the number
286+ of ``matchParams`` keys: more keys = more specific. If one of the duplicates matched
287+ by ``name`` it is always considered more specific than one that did not.
288+
289+ This replaces the old per-type hacks (``FreeDiskSpaceMB`` > ``FreeDiskSpaceGB`` >
290+ ``FreeDiskSpaceTB``) with a generic rule that works for any policy type .
291+ """
292+ from collections import defaultdict
293+
294+ # Group policies by policyType
295+ byType = defaultdict ( list )
296+ for entry in policiesThatApply :
297+ policyName , policyType , policyConfigParams , policyMatchParams = entry
298+ byType [ policyType ]. append ( entry )
299+
300+ result = []
301+ for policyType , entries in byType . items () :
302+ if len ( entries ) == 1 :
303+ result . extend ( entries )
304+ continue
288305
289- return policiesThatApply
306+ # Multiple policies of the same type matched — keep only the most specific.
307+ # Specificity = number of matchParams keys; ties broken by name-match presence.
308+ def specificity (entry ):
309+ matchParams = entry [3 ] # policyMatchParams
310+ nameMatch = 1 if "name" in matchParams else 0
311+ return (nameMatch , len (matchParams ))
312+
313+ most_specific = max (entries , key = specificity )
314+ result .append (most_specific )
315+ gLogger .debug (
316+ f"postProcessing: multiple { policyType !r} policies matched; "
317+ f"keeping { most_specific [0 ]!r} , dropping { [e [0 ] for e in entries if e is not most_specific ]} "
318+ )
319+
320+ return result
0 commit comments