1    	import sys
2    	import xml.dom.minidom
3    	from collections.abc import Iterable
4    	from enum import Enum
5    	from typing import Any, TypeVar, cast
6    	from xml.dom.minidom import parseString
7    	
8    	import pcs.cli.constraint_order.command as order_command
9    	from pcs import utils
10   	from pcs.cli.common import parse_args
11   	from pcs.cli.common.errors import (
12   	    CmdLineInputError,
13   	    raise_command_replaced,
14   	)
15   	from pcs.cli.common.output import (
16   	    INDENT_STEP,
17   	    lines_to_str,
18   	)
19   	from pcs.cli.constraint.location.command import (
20   	    RESOURCE_TYPE_REGEXP,
21   	    RESOURCE_TYPE_RESOURCE,
22   	)
23   	from pcs.cli.constraint.output import (
24   	    CibConstraintLocationAnyDto,
25   	    filter_constraints_by_rule_expired_status,
26   	    location,
27   	    print_config,
28   	)
29   	from pcs.cli.reports import process_library_reports
30   	from pcs.cli.reports.output import (
31   	    deprecation_warning,
32   	    print_to_stderr,
33   	    warn,
34   	)
35   	from pcs.common import (
36   	    const,
37   	    pacemaker,
38   	    reports,
39   	)
40   	from pcs.common.pacemaker.constraint import (
41   	    CibConstraintColocationSetDto,
42   	    CibConstraintLocationSetDto,
43   	    CibConstraintOrderSetDto,
44   	    CibConstraintsDto,
45   	    CibConstraintTicketSetDto,
46   	    get_all_constraints_ids,
47   	)
48   	from pcs.common.pacemaker.resource.list import CibResourcesDto
49   	from pcs.common.pacemaker.types import CibResourceDiscovery
50   	from pcs.common.reports import ReportItem
51   	from pcs.common.str_tools import (
52   	    format_list,
53   	    indent,
54   	)
55   	from pcs.common.types import (
56   	    StringCollection,
57   	    StringIterable,
58   	    StringSequence,
59   	)
60   	from pcs.lib.cib.constraint.order import ATTRIB as order_attrib
61   	from pcs.lib.node import get_existing_nodes_names
62   	from pcs.lib.pacemaker.values import (
63   	    SCORE_INFINITY,
64   	    is_true,
65   	    sanitize_id,
66   	)
67   	
68   	DEFAULT_ACTION = const.PCMK_ACTION_START
69   	DEFAULT_ROLE = const.PCMK_ROLE_STARTED
70   	
71   	OPTIONS_SYMMETRICAL = order_attrib["symmetrical"]
72   	
73   	LOCATION_NODE_VALIDATION_SKIP_MSG = (
74   	    "Validation for node existence in the cluster will be skipped"
75   	)
76   	STANDALONE_SCORE_MSG = (
77   	    "Specifying score as a standalone value is deprecated and "
78   	    "might be removed in a future release, use score=value instead"
79   	)
80   	
81   	
82   	class CrmRuleReturnCode(Enum):
83   	    IN_EFFECT = 0
84   	    EXPIRED = 110
85   	    TO_BE_IN_EFFECT = 111
86   	
87   	
88   	def constraint_order_cmd(lib, argv, modifiers):
89   	    sub_cmd = "config" if not argv else argv.pop(0)
90   	
91   	    try:
92   	        if sub_cmd == "set":
93   	            order_command.create_with_set(lib, argv, modifiers)
94   	        elif sub_cmd in ["remove", "delete"]:
95   	            order_rm(lib, argv, modifiers)
96   	        elif sub_cmd == "show":
97   	            raise_command_replaced(
98   	                ["pcs constraint order config"], pcs_version="0.12"
99   	            )
100  	        elif sub_cmd == "config":
101  	            order_command.config_cmd(lib, argv, modifiers)
102  	        else:
103  	            order_start(lib, [sub_cmd] + argv, modifiers)
104  	    except CmdLineInputError as e:
105  	        utils.exit_on_cmdline_input_error(e, "constraint", ["order", sub_cmd])
106  	
107  	
108  	def config_cmd(
109  	    lib: Any, argv: list[str], modifiers: parse_args.InputModifiers
110  	) -> None:
111  	    modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all")
112  	    if argv:
113  	        raise CmdLineInputError()
114  	
115  	    print_config(
116  	        cast(
117  	            CibConstraintsDto,
118  	            lib.constraint.get_config(evaluate_rules=True),
119  	        ),
120  	        modifiers,
121  	    )
122  	
123  	
124  	def _validate_constraint_resource(cib_dom, resource_id):
125  	    (
126  	        resource_valid,
127  	        resource_error,
128  	        dummy_correct_id,
129  	    ) = utils.validate_constraint_resource(cib_dom, resource_id)
130  	    if not resource_valid:
131  	        utils.err(resource_error)
132  	
133  	
134  	def _validate_resources_not_in_same_group(cib_dom, resource1, resource2):
135  	    if not utils.validate_resources_not_in_same_group(
136  	        cib_dom, resource1, resource2
137  	    ):
138  	        utils.err(
139  	            "Cannot create an order constraint for resources in the same group"
140  	        )
141  	
142  	
143  	# Syntax: colocation add [role] <src> with [role] <tgt> [score] [options]
144  	# possible commands:
145  	#        <src> with        <tgt> [score] [options]
146  	#        <src> with <role> <tgt> [score] [options]
147  	# <role> <src> with        <tgt> [score] [options]
148  	# <role> <src> with <role> <tgt> [score] [options]
149  	# Specifying score as a single argument is deprecated, though. The correct way
150  	# is score=value in options.
151  	def colocation_add(lib, argv, modifiers):  # noqa: PLR0912, PLR0915
152  	    """
153  	    Options:
154  	      * -f - CIB file
155  	      * --force - allow constraint on any resource, allow duplicate constraints
156  	    """
157  	
158  	    def _parse_score_options(argv):
159  	        # When passed an array of arguments if the first argument doesn't have
160  	        # an '=' then it's the score, otherwise they're all arguments. Return a
161  	        # tuple with the score and array of name,value pairs
162  	        """
163  	        Commandline options: no options
164  	        """
165  	        if not argv:
166  	            return None, []
167  	        score = None
168  	        if "=" not in argv[0]:
169  	            score = argv.pop(0)
170  	            # TODO added to pcs in the first 0.12.x version
171  	            deprecation_warning(STANDALONE_SCORE_MSG)
172  	
173  	        # create a list of 2-tuples (name, value)
174  	        arg_array = [
175  	            parse_args.split_option(arg, allow_empty_value=False)
176  	            for arg in argv
177  	        ]
178  	        return score, arg_array
179  	
180  	    def _validate_and_prepare_role(new_roles_supported, role):
181  	        if role is None:
182  	            return ""
183  	        role_cleaned = role.lower().capitalize()
184  	        if role_cleaned not in const.PCMK_ROLES:
185  	            utils.err(
186  	                "invalid role value '{0}', allowed values are: {1}".format(
187  	                    role, format_list(const.PCMK_ROLES)
188  	                )
189  	            )
190  	        return pacemaker.role.get_value_for_cib(
191  	            role_cleaned, new_roles_supported
192  	        )
193  	
194  	    del lib
195  	    modifiers.ensure_only_supported("-f", "--force")
196  	    if len(argv) < 3:
197  	        raise CmdLineInputError()
198  	
199  	    role1_candidate = None
200  	    role2_candidate = None
201  	
202  	    if argv[2] == "with":
203  	        role1_candidate = argv.pop(0)
204  	        resource1 = argv.pop(0)
205  	    elif argv[1] == "with":
206  	        resource1 = argv.pop(0)
207  	    else:
208  	        raise CmdLineInputError()
209  	
210  	    if argv.pop(0) != "with":
211  	        raise CmdLineInputError()
212  	    if "with" in argv:
213  	        raise CmdLineInputError(
214  	            message="Multiple 'with's cannot be specified.",
215  	            hint=(
216  	                "Use the 'pcs constraint colocation set' command if you want "
217  	                "to create a constraint for more than two resources."
218  	            ),
219  	            show_both_usage_and_message=True,
220  	        )
221  	
222  	    if not argv:
223  	        raise CmdLineInputError()
224  	    if len(argv) == 1 or utils.is_score_or_opt(argv[1]):
225  	        resource2 = argv.pop(0)
226  	    else:
227  	        role2_candidate = argv.pop(0)
228  	        resource2 = argv.pop(0)
229  	
230  	    score, nv_pairs = _parse_score_options(argv)
231  	    influence_attr_set = any(name == "influence" for name, _ in nv_pairs)
232  	
233  	    cib_dom = (
234  	        utils.cluster_upgrade_to_version(
235  	            const.PCMK_COLOCATION_INFLUENCE_CIB_VERSION
236  	        )
237  	        if influence_attr_set
238  	        else utils.get_cib_dom()
239  	    )
240  	    new_roles_supported = utils.isCibVersionSatisfied(
241  	        cib_dom, const.PCMK_NEW_ROLES_CIB_VERSION
242  	    )
243  	
244  	    role1 = _validate_and_prepare_role(new_roles_supported, role1_candidate)
245  	    role2 = _validate_and_prepare_role(new_roles_supported, role2_candidate)
246  	    _validate_constraint_resource(cib_dom, resource1)
247  	    _validate_constraint_resource(cib_dom, resource2)
248  	
249  	    id_in_nvpairs = None
250  	    for name, value in nv_pairs:
251  	        if name == "id":
252  	            id_valid, id_error = utils.validate_xml_id(value, "constraint id")
253  	            if not id_valid:
254  	                utils.err(id_error)
255  	            if utils.does_id_exist(cib_dom, value):
256  	                utils.err(
257  	                    "id '%s' is already in use, please specify another one"
258  	                    % value
259  	                )
260  	            id_in_nvpairs = True
261  	        elif name == "score":
262  	            score = value
263  	    if score is None:
264  	        score = SCORE_INFINITY
265  	    if not id_in_nvpairs:
266  	        nv_pairs.append(
267  	            (
268  	                "id",
269  	                utils.find_unique_id(
270  	                    cib_dom,
271  	                    "colocation-%s-%s-%s" % (resource1, resource2, score),
272  	                ),
273  	            )
274  	        )
275  	
276  	    (dom, constraintsElement) = getCurrentConstraints(cib_dom)
277  	
278  	    # If one role is specified, the other should default to "started"
279  	    if role1 != "" and role2 == "":
280  	        role2 = DEFAULT_ROLE
281  	    if role2 != "" and role1 == "":
282  	        role1 = DEFAULT_ROLE
283  	    element = dom.createElement("rsc_colocation")
284  	    element.setAttribute("rsc", resource1)
285  	    element.setAttribute("with-rsc", resource2)
286  	    element.setAttribute("score", score)
287  	    if role1 != "":
288  	        element.setAttribute("rsc-role", role1)
289  	    if role2 != "":
290  	        element.setAttribute("with-rsc-role", role2)
291  	    for nv_pair in nv_pairs:
292  	        element.setAttribute(nv_pair[0], nv_pair[1])
293  	    if not modifiers.get("--force"):
294  	
295  	        def _constraint_export(constraint_info):
296  	            options_dict = constraint_info["options"]
297  	            co_resource1 = options_dict.get("rsc", "")
298  	            co_resource2 = options_dict.get("with-rsc", "")
299  	            co_id = options_dict.get("id", "")
300  	            co_score = options_dict.get("score", "")
301  	            score_text = "(score:" + co_score + ")"
302  	            console_option_list = [
303  	                f"({option[0]}:{option[1]})"
304  	                for option in sorted(options_dict.items())
305  	                if option[0] not in ("rsc", "with-rsc", "id", "score")
306  	            ]
307  	            console_option_list.append(f"(id:{co_id})")
308  	            return " ".join(
309  	                [co_resource1, "with", co_resource2, score_text]
310  	                + console_option_list
311  	            )
312  	
313  	        duplicates = colocation_find_duplicates(constraintsElement, element)
314  	        if duplicates:
315  	            utils.err(
316  	                "duplicate constraint already exists, use --force to override\n"
317  	                + "\n".join(
318  	                    [
319  	                        "  "
320  	                        + _constraint_export(
321  	                            {"options": dict(dup.attributes.items())}
322  	                        )
323  	                        for dup in duplicates
324  	                    ]
325  	                )
326  	            )
327  	    constraintsElement.appendChild(element)
328  	    utils.replace_cib_configuration(dom)
329  	
330  	
331  	def colocation_find_duplicates(dom, constraint_el):
332  	    """
333  	    Commandline options: no options
334  	    """
335  	    new_roles_supported = utils.isCibVersionSatisfied(
336  	        dom, const.PCMK_NEW_ROLES_CIB_VERSION
337  	    )
338  	
339  	    def normalize(const_el):
340  	        return (
341  	            const_el.getAttribute("rsc"),
342  	            const_el.getAttribute("with-rsc"),
343  	            pacemaker.role.get_value_for_cib(
344  	                const_el.getAttribute("rsc-role").capitalize() or DEFAULT_ROLE,
345  	                new_roles_supported,
346  	            ),
347  	            pacemaker.role.get_value_for_cib(
348  	                const_el.getAttribute("with-rsc-role").capitalize()
349  	                or DEFAULT_ROLE,
350  	                new_roles_supported,
351  	            ),
352  	        )
353  	
354  	    normalized_el = normalize(constraint_el)
355  	    return [
356  	        other_el
357  	        for other_el in dom.getElementsByTagName("rsc_colocation")
358  	        if not other_el.getElementsByTagName("resource_set")
359  	        and constraint_el is not other_el
360  	        and normalized_el == normalize(other_el)
361  	    ]
362  	
363  	
364  	def order_rm(lib, argv, modifiers):
365  	    """
366  	    Options:
367  	      * -f - CIB file
368  	    """
369  	    del lib
370  	    modifiers.ensure_only_supported("-f")
371  	    if not argv:
372  	        raise CmdLineInputError()
373  	
374  	    elementFound = False
375  	    (dom, constraintsElement) = getCurrentConstraints()
376  	
377  	    for resource in argv:
378  	        for ord_loc in constraintsElement.getElementsByTagName("rsc_order")[:]:
379  	            if (
380  	                ord_loc.getAttribute("first") == resource
381  	                or ord_loc.getAttribute("then") == resource
382  	            ):
383  	                constraintsElement.removeChild(ord_loc)
384  	                elementFound = True
385  	
386  	        resource_refs_to_remove = []
387  	        for ord_set in constraintsElement.getElementsByTagName("resource_ref"):
388  	            if ord_set.getAttribute("id") == resource:
389  	                resource_refs_to_remove.append(ord_set)
390  	                elementFound = True
391  	
392  	        for res_ref in resource_refs_to_remove:
393  	            res_set = res_ref.parentNode
394  	            res_order = res_set.parentNode
395  	
396  	            res_ref.parentNode.removeChild(res_ref)
397  	            if not res_set.getElementsByTagName("resource_ref"):
398  	                res_set.parentNode.removeChild(res_set)
399  	                if not res_order.getElementsByTagName("resource_set"):
400  	                    res_order.parentNode.removeChild(res_order)
401  	
402  	    if elementFound:
403  	        utils.replace_cib_configuration(dom)
404  	    else:
405  	        utils.err("No matching resources found in ordering list")
406  	
407  	
408  	def order_start(lib, argv, modifiers):
409  	    """
410  	    Options:
411  	      * -f - CIB file
412  	      * --force - allow constraint for any resource, allow duplicate constraints
413  	    """
414  	    del lib
415  	    modifiers.ensure_only_supported("-f", "--force")
416  	    if len(argv) < 3:
417  	        raise CmdLineInputError()
418  	
419  	    first_action = DEFAULT_ACTION
420  	    then_action = DEFAULT_ACTION
421  	    action = argv[0]
422  	    if action in const.PCMK_ACTIONS:
423  	        first_action = action
424  	        argv.pop(0)
425  	
426  	    resource1 = argv.pop(0)
427  	    if argv.pop(0) != "then":
428  	        raise CmdLineInputError()
429  	
430  	    if not argv:
431  	        raise CmdLineInputError()
432  	
433  	    action = argv[0]
434  	    if action in const.PCMK_ACTIONS:
435  	        then_action = action
436  	        argv.pop(0)
437  	
438  	    if not argv:
439  	        raise CmdLineInputError()
440  	    resource2 = argv.pop(0)
441  	
442  	    order_options = []
443  	    if argv:
444  	        order_options = order_options + argv[:]
445  	    if "then" in order_options:
446  	        raise CmdLineInputError(
447  	            message="Multiple 'then's cannot be specified.",
448  	            hint=(
449  	                "Use the 'pcs constraint order set' command if you want to "
450  	                "create a constraint for more than two resources."
451  	            ),
452  	            show_both_usage_and_message=True,
453  	        )
454  	
455  	    order_options.append("first-action=" + first_action)
456  	    order_options.append("then-action=" + then_action)
457  	    _order_add(resource1, resource2, order_options, modifiers)
458  	
459  	
460  	def _order_add(resource1, resource2, options_list, modifiers):  # noqa: PLR0912, PLR0915
461  	    """
462  	    Commandline options:
463  	      * -f - CIB file
464  	      * --force - allow constraint for any resource, allow duplicate constraints
465  	    """
466  	    cib_dom = utils.get_cib_dom()
467  	    _validate_constraint_resource(cib_dom, resource1)
468  	    _validate_constraint_resource(cib_dom, resource2)
469  	
470  	    _validate_resources_not_in_same_group(cib_dom, resource1, resource2)
471  	
472  	    order_options = []
473  	    id_specified = False
474  	    sym = None
475  	    for arg in options_list:
476  	        if arg == "symmetrical":
477  	            sym = "true"
478  	        elif arg == "nonsymmetrical":
479  	            sym = "false"
480  	        else:
481  	            name, value = parse_args.split_option(arg, allow_empty_value=False)
482  	            if name == "id":
483  	                id_valid, id_error = utils.validate_xml_id(
484  	                    value, "constraint id"
485  	                )
486  	                if not id_valid:
487  	                    utils.err(id_error)
488  	                if utils.does_id_exist(cib_dom, value):
489  	                    utils.err(
490  	                        "id '%s' is already in use, please specify another one"
491  	                        % value
492  	                    )
493  	                id_specified = True
494  	                order_options.append((name, value))
495  	            elif name == "symmetrical":
496  	                if value.lower() in OPTIONS_SYMMETRICAL:
497  	                    sym = value.lower()
498  	                else:
499  	                    utils.err(
500  	                        "invalid symmetrical value '%s', allowed values are: %s"
501  	                        % (value, ", ".join(OPTIONS_SYMMETRICAL))
502  	                    )
503  	            else:
504  	                order_options.append((name, value))
505  	    if sym:
506  	        order_options.append(("symmetrical", sym))
507  	
508  	    options = ""
509  	    if order_options:
510  	        options = " (Options: %s)" % " ".join(
511  	            [
512  	                "%s=%s" % (name, value)
513  	                for name, value in order_options
514  	                if name not in ("kind", "score")
515  	            ]
516  	        )
517  	
518  	    scorekind = "kind: Mandatory"
519  	    id_suffix = "mandatory"
520  	    for opt in order_options:
521  	        if opt[0] == "score":
522  	            scorekind = "score: " + opt[1]
523  	            id_suffix = opt[1]
524  	            # TODO deprecated in pacemaker 2, to be removed in pacemaker 3
525  	            # added to pcs after 0.11.7
526  	            deprecation_warning(
527  	                reports.messages.DeprecatedOption(opt[0], []).message
528  	            )
529  	            break
530  	        if opt[0] == "kind":
531  	            scorekind = "kind: " + opt[1]
532  	            id_suffix = opt[1]
533  	            break
534  	
535  	    if not id_specified:
536  	        order_id = "order-" + resource1 + "-" + resource2 + "-" + id_suffix
537  	        order_id = utils.find_unique_id(cib_dom, order_id)
538  	        order_options.append(("id", order_id))
539  	
540  	    (dom, constraintsElement) = getCurrentConstraints()
541  	    element = dom.createElement("rsc_order")
542  	    element.setAttribute("first", resource1)
543  	    element.setAttribute("then", resource2)
544  	    for order_opt in order_options:
545  	        element.setAttribute(order_opt[0], order_opt[1])
546  	    constraintsElement.appendChild(element)
547  	    if not modifiers.get("--force"):
548  	
549  	        def _constraint_export(constraint_info):
550  	            options = constraint_info["options"]
551  	            oc_resource1 = options.get("first", "")
552  	            oc_resource2 = options.get("then", "")
553  	            first_action = options.get("first-action", "")
554  	            then_action = options.get("then-action", "")
555  	            oc_id = options.get("id", "")
556  	            oc_score = options.get("score", "")
557  	            oc_kind = options.get("kind", "")
558  	            oc_sym = ""
559  	            oc_id_out = ""
560  	            oc_options = ""
561  	            if "symmetrical" in options and not is_true(
562  	                options.get("symmetrical", "false")
563  	            ):
564  	                oc_sym = "(non-symmetrical)"
565  	            if oc_kind != "":
566  	                score_text = "(kind:" + oc_kind + ")"
567  	            elif oc_kind == "" and oc_score == "":
568  	                score_text = "(kind:Mandatory)"
569  	            else:
570  	                score_text = "(score:" + oc_score + ")"
571  	            oc_id_out = "(id:" + oc_id + ")"
572  	            already_processed_options = (
573  	                "first",
574  	                "then",
575  	                "first-action",
576  	                "then-action",
577  	                "id",
578  	                "score",
579  	                "kind",
580  	                "symmetrical",
581  	            )
582  	            oc_options = " ".join(
583  	                [
584  	                    f"{name}={value}"
585  	                    for name, value in options.items()
586  	                    if name not in already_processed_options
587  	                ]
588  	            )
589  	            if oc_options:
590  	                oc_options = "(Options: " + oc_options + ")"
591  	            return " ".join(
592  	                [
593  	                    arg
594  	                    for arg in [
595  	                        first_action,
596  	                        oc_resource1,
597  	                        "then",
598  	                        then_action,
599  	                        oc_resource2,
600  	                        score_text,
601  	                        oc_sym,
602  	                        oc_options,
603  	                        oc_id_out,
604  	                    ]
605  	                    if arg
606  	                ]
607  	            )
608  	
609  	        duplicates = order_find_duplicates(constraintsElement, element)
610  	        if duplicates:
611  	            utils.err(
612  	                "duplicate constraint already exists, use --force to override\n"
613  	                + "\n".join(
614  	                    [
615  	                        "  "
616  	                        + _constraint_export(
617  	                            {"options": dict(dup.attributes.items())}
618  	                        )
619  	                        for dup in duplicates
620  	                    ]
621  	                )
622  	            )
623  	    print_to_stderr(f"Adding {resource1} {resource2} ({scorekind}){options}")
624  	    utils.replace_cib_configuration(dom)
625  	
626  	
627  	def order_find_duplicates(dom, constraint_el):
628  	    """
629  	    Commandline options: no options
630  	    """
631  	
632  	    def normalize(constraint_el):
633  	        return (
634  	            constraint_el.getAttribute("first"),
635  	            constraint_el.getAttribute("then"),
636  	            constraint_el.getAttribute("first-action").lower()
637  	            or DEFAULT_ACTION,
638  	            constraint_el.getAttribute("then-action").lower() or DEFAULT_ACTION,
639  	        )
640  	
641  	    normalized_el = normalize(constraint_el)
642  	    return [
643  	        other_el
644  	        for other_el in dom.getElementsByTagName("rsc_order")
645  	        if not other_el.getElementsByTagName("resource_set")
646  	        and constraint_el is not other_el
647  	        and normalized_el == normalize(other_el)
648  	    ]
649  	
650  	
651  	_SetConstraint = TypeVar(
652  	    "_SetConstraint",
653  	    CibConstraintLocationSetDto,
654  	    CibConstraintColocationSetDto,
655  	    CibConstraintOrderSetDto,
656  	    CibConstraintTicketSetDto,
657  	)
658  	
659  	
660  	def _filter_set_constraints_by_resources(
661  	    constraints_dto: Iterable[_SetConstraint], resources: set[str]
662  	) -> list[_SetConstraint]:
663  	    return [
664  	        constraint_set_dto
665  	        for constraint_set_dto in constraints_dto
666  	        if any(
667  	            set(resource_set.resources_ids) & resources
668  	            for resource_set in constraint_set_dto.resource_sets
669  	        )
670  	    ]
671  	
672  	
673  	def _filter_constraints_by_resources(
674  	    constraints_dto: CibConstraintsDto,
675  	    resources: StringIterable,
676  	    patterns: StringIterable,
677  	) -> CibConstraintsDto:
678  	    required_resources_set = set(resources)
679  	    required_patterns_set = set(patterns)
680  	    return CibConstraintsDto(
681  	        location=[
682  	            constraint_dto
683  	            for constraint_dto in constraints_dto.location
684  	            if (
685  	                constraint_dto.resource_id is not None
686  	                and constraint_dto.resource_id in required_resources_set
687  	            )
688  	            or (
689  	                constraint_dto.resource_pattern is not None
690  	                and constraint_dto.resource_pattern in required_patterns_set
691  	            )
692  	        ],
693  	        location_set=_filter_set_constraints_by_resources(
694  	            constraints_dto.location_set, required_resources_set
695  	        ),
696  	        colocation=[
697  	            constraint_dto
698  	            for constraint_dto in constraints_dto.colocation
699  	            if {constraint_dto.resource_id, constraint_dto.with_resource_id}
700  	            & required_resources_set
701  	        ],
702  	        colocation_set=_filter_set_constraints_by_resources(
703  	            constraints_dto.colocation_set, required_resources_set
704  	        ),
705  	        order=[
706  	            constraint_dto
707  	            for constraint_dto in constraints_dto.order
708  	            if {
709  	                constraint_dto.first_resource_id,
710  	                constraint_dto.then_resource_id,
711  	            }
712  	            & required_resources_set
713  	        ],
714  	        order_set=_filter_set_constraints_by_resources(
715  	            constraints_dto.order_set, required_resources_set
716  	        ),
717  	        ticket=[
718  	            constraint_dto
719  	            for constraint_dto in constraints_dto.ticket
720  	            if constraint_dto.resource_id in required_resources_set
721  	        ],
722  	        ticket_set=_filter_set_constraints_by_resources(
723  	            constraints_dto.ticket_set, required_resources_set
724  	        ),
725  	    )
726  	
727  	
728  	def _filter_location_by_node_base(
729  	    constraint_dtos: Iterable[CibConstraintLocationAnyDto],
730  	    nodes: StringCollection,
731  	) -> list[CibConstraintLocationAnyDto]:
732  	    return [
733  	        constraint_dto
734  	        for constraint_dto in constraint_dtos
735  	        if constraint_dto.attributes.node is not None
736  	        and constraint_dto.attributes.node in nodes
737  	    ]
738  	
739  	
740  	def location_config_cmd(
741  	    lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers
742  	) -> None:
743  	    """
744  	    Options:
745  	      * --all - print expired constraints
746  	      * --full - print all details
747  	      * -f - CIB file
748  	    """
749  	    modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all")
750  	    filter_type: str | None = None
751  	    filter_items: parse_args.Argv = []
752  	    if argv:
753  	        filter_type, *filter_items = argv
754  	        allowed_types = ("resources", "nodes")
755  	        if filter_type not in allowed_types:
756  	            raise CmdLineInputError(
757  	                f"Unknown keyword '{filter_type}'. Allowed keywords: "
758  	                f"{format_list(allowed_types)}"
759  	            )
760  	        if modifiers.get_output_format() != parse_args.OUTPUT_FORMAT_VALUE_TEXT:
761  	            raise CmdLineInputError(
762  	                "Output formats other than 'text' are not supported together "
763  	                "with grouping and filtering by nodes or resources"
764  	            )
765  	
766  	    constraints_dto = filter_constraints_by_rule_expired_status(
767  	        lib.constraint.get_config(evaluate_rules=True),
768  	        modifiers.is_specified("--all"),
769  	    )
770  	
771  	    constraints_dto = CibConstraintsDto(
772  	        location=constraints_dto.location,
773  	        location_set=constraints_dto.location_set,
774  	    )
775  	
776  	    def _print_lines(lines: StringSequence) -> None:
777  	        if lines:
778  	            print("Location Constraints:")
779  	            print(lines_to_str(indent(lines, indent_step=INDENT_STEP)))
780  	
781  	    if filter_type == "resources":
782  	        if filter_items:
783  	            resources = []
784  	            patterns = []
785  	            for item in filter_items:
786  	                item_type, item_value = parse_args.parse_typed_arg(
787  	                    item,
788  	                    [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP],
789  	                    RESOURCE_TYPE_RESOURCE,
790  	                )
791  	                if item_type == RESOURCE_TYPE_RESOURCE:
792  	                    resources.append(item_value)
793  	                elif item_type == RESOURCE_TYPE_REGEXP:
794  	                    patterns.append(item_value)
795  	            constraints_dto = _filter_constraints_by_resources(
796  	                constraints_dto, resources, patterns
797  	            )
798  	        _print_lines(
799  	            location.constraints_to_grouped_by_resource_text(
800  	                constraints_dto.location,
801  	                modifiers.is_specified("--full"),
802  	            )
803  	        )
804  	        return
805  	    if filter_type == "nodes":
806  	        if filter_items:
807  	            constraints_dto = CibConstraintsDto(
808  	                location=_filter_location_by_node_base(
809  	                    constraints_dto.location, filter_items
810  	                ),
811  	                location_set=_filter_location_by_node_base(
812  	                    constraints_dto.location_set, filter_items
813  	                ),
814  	            )
815  	        _print_lines(
816  	            location.constraints_to_grouped_by_node_text(
817  	                constraints_dto.location,
818  	                modifiers.is_specified("--full"),
819  	            )
820  	        )
821  	        return
822  	
823  	    print_config(constraints_dto, modifiers)
824  	
825  	
826  	def _verify_node_name(node, existing_nodes):
827  	    report_list = []
828  	    if node not in existing_nodes:
829  	        report_list.append(
830  	            ReportItem.error(
831  	                reports.messages.NodeNotFound(node),
832  	                force_code=reports.codes.FORCE,
833  	            )
834  	        )
835  	    return report_list
836  	
837  	
838  	def _verify_score(score):
839  	    if not utils.is_score(score):
840  	        utils.err(
841  	            "invalid score '%s', use integer or INFINITY or -INFINITY" % score
842  	        )
843  	
844  	
845  	def location_prefer(  # noqa: PLR0912
846  	    lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers
847  	) -> None:
848  	    """
849  	    Options:
850  	      * --force - allow unknown options, allow constraint for any resource type
851  	      * -f - CIB file
852  	    """
853  	    modifiers.ensure_only_supported("--force", "-f")
854  	    rsc = argv.pop(0)
855  	    prefer_option = argv.pop(0)
856  	
857  	    dummy_rsc_type, rsc_value = parse_args.parse_typed_arg(
858  	        rsc,
859  	        [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP],
860  	        RESOURCE_TYPE_RESOURCE,
861  	    )
862  	
863  	    if prefer_option == "prefers":
864  	        prefer = True
865  	    elif prefer_option == "avoids":
866  	        prefer = False
867  	    else:
868  	        raise CmdLineInputError()
869  	
870  	    skip_node_check = False
871  	    existing_nodes: list[str] = []
872  	    if modifiers.is_specified("-f") or modifiers.get("--force"):
873  	        skip_node_check = True
874  	        warn(LOCATION_NODE_VALIDATION_SKIP_MSG)
875  	    else:
876  	        lib_env = utils.get_lib_env()
877  	        existing_nodes, report_list = get_existing_nodes_names(
878  	            corosync_conf=lib_env.get_corosync_conf(),
879  	            cib=lib_env.get_cib(),
880  	        )
881  	        if report_list:
882  	            process_library_reports(report_list)
883  	
884  	    report_list = []
885  	    parameters_list = []
886  	    for nodeconf in argv:
887  	        nodeconf_a = nodeconf.split("=", 1)
888  	        node = nodeconf_a[0]
889  	        if not skip_node_check:
890  	            report_list += _verify_node_name(node, existing_nodes)
891  	        if len(nodeconf_a) == 1:
892  	            score = "INFINITY" if prefer else "-INFINITY"
893  	        else:
894  	            score = nodeconf_a[1]
895  	            _verify_score(score)
896  	            if not prefer:
897  	                score = score[1:] if score[0] == "-" else "-" + score
898  	
899  	        parameters_list.append(
900  	            [
901  	                sanitize_id(f"location-{rsc_value}-{node}-{score}"),
902  	                rsc,
903  	                node,
904  	                f"score={score}",
905  	            ]
906  	        )
907  	
908  	    if report_list:
909  	        process_library_reports(report_list)
910  	
911  	    modifiers = modifiers.get_subset("--force", "-f")
912  	
913  	    for parameters in parameters_list:
914  	        location_add(lib, parameters, modifiers, skip_score_and_node_check=True)
915  	
916  	
917  	def location_add(  # noqa: PLR0912, PLR0915
918  	    lib: Any,
919  	    argv: parse_args.Argv,
920  	    modifiers: parse_args.InputModifiers,
921  	    skip_score_and_node_check: bool = False,
922  	) -> None:
923  	    """
924  	    Options:
925  	      * --force - allow unknown options, allow constraint for any resource type
926  	      * -f - CIB file
927  	    """
928  	    del lib
929  	    modifiers.ensure_only_supported("--force", "-f")
930  	    if len(argv) < 4:
931  	        raise CmdLineInputError()
932  	
933  	    constraint_id = argv.pop(0)
934  	    rsc_type, rsc_value = parse_args.parse_typed_arg(
935  	        argv.pop(0),
936  	        [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP],
937  	        RESOURCE_TYPE_RESOURCE,
938  	    )
939  	    node = argv.pop(0)
940  	    score = None
941  	    if "=" not in argv[0]:
942  	        score = argv.pop(0)
943  	        # TODO added to pcs in the first 0.12.x version
944  	        deprecation_warning(STANDALONE_SCORE_MSG)
945  	    options = []
946  	    # For now we only allow setting resource-discovery and score
947  	    for arg in argv:
948  	        name, value = parse_args.split_option(arg, allow_empty_value=False)
949  	        if name == "score":
950  	            score = value
951  	        elif name == "resource-discovery":
952  	            if not modifiers.get("--force"):
953  	                allowed_discovery = list(
954  	                    map(
955  	                        str,
956  	                        [
957  	                            CibResourceDiscovery.ALWAYS,
958  	                            CibResourceDiscovery.EXCLUSIVE,
959  	                            CibResourceDiscovery.NEVER,
960  	                        ],
961  	                    )
962  	                )
963  	                if value not in allowed_discovery:
964  	                    utils.err(
965  	                        (
966  	                            "invalid {0} value '{1}', allowed values are: {2}"
967  	                            ", use --force to override"
968  	                        ).format(name, value, format_list(allowed_discovery))
969  	                    )
970  	            options.append([name, value])
971  	        elif modifiers.get("--force"):
972  	            options.append([name, value])
973  	        else:
974  	            utils.err("bad option '%s', use --force to override" % name)
975  	    if score is None:
976  	        score = "INFINITY"
977  	
978  	    # Verify that specified node exists in the cluster and score is valid
979  	    if not skip_score_and_node_check:
980  	        if modifiers.is_specified("-f") or modifiers.get("--force"):
981  	            warn(LOCATION_NODE_VALIDATION_SKIP_MSG)
982  	        else:
983  	            lib_env = utils.get_lib_env()
984  	            existing_nodes, report_list = get_existing_nodes_names(
985  	                corosync_conf=lib_env.get_corosync_conf(),
986  	                cib=lib_env.get_cib(),
987  	            )
988  	            report_list += _verify_node_name(node, existing_nodes)
989  	            if report_list:
990  	                process_library_reports(report_list)
991  	        _verify_score(score)
992  	
993  	    id_valid, id_error = utils.validate_xml_id(constraint_id, "constraint id")
994  	    if not id_valid:
995  	        utils.err(id_error)
996  	
997  	    dom = utils.get_cib_dom()
998  	
999  	    if rsc_type == RESOURCE_TYPE_RESOURCE:
1000 	        (
1001 	            rsc_valid,
1002 	            rsc_error,
1003 	            dummy_correct_id,
1004 	        ) = utils.validate_constraint_resource(dom, rsc_value)
1005 	        if not rsc_valid:
1006 	            utils.err(rsc_error)
1007 	
1008 	    # Verify current constraint doesn't already exist
1009 	    # If it does we replace it with the new constraint
1010 	    dummy_dom, constraintsElement = getCurrentConstraints(dom)
1011 	    # If the id matches, or the rsc & node match, then we replace/remove
1012 	    elementsToRemove = [
1013 	        rsc_loc
1014 	        for rsc_loc in constraintsElement.getElementsByTagName("rsc_location")
1015 	        if rsc_loc.getAttribute("id") == constraint_id
1016 	        or (
1017 	            rsc_loc.getAttribute("node") == node
1018 	            and (
1019 	                (
1020 	                    rsc_type == RESOURCE_TYPE_RESOURCE
1021 	                    and rsc_loc.getAttribute("rsc") == rsc_value
1022 	                )
1023 	                or (
1024 	                    rsc_type == RESOURCE_TYPE_REGEXP
1025 	                    and rsc_loc.getAttribute("rsc-pattern") == rsc_value
1026 	                )
1027 	            )
1028 	        )
1029 	    ]
1030 	    for etr in elementsToRemove:
1031 	        constraintsElement.removeChild(etr)
1032 	
1033 	    element = dom.createElement("rsc_location")
1034 	    element.setAttribute("id", constraint_id)
1035 	    if rsc_type == RESOURCE_TYPE_RESOURCE:
1036 	        element.setAttribute("rsc", rsc_value)
1037 	    elif rsc_type == RESOURCE_TYPE_REGEXP:
1038 	        element.setAttribute("rsc-pattern", rsc_value)
1039 	    element.setAttribute("node", node)
1040 	    element.setAttribute("score", score)
1041 	    for option in options:
1042 	        element.setAttribute(option[0], option[1])
1043 	    constraintsElement.appendChild(element)
1044 	
1045 	    utils.replace_cib_configuration(dom)
1046 	
1047 	
1048 	# Grabs the current constraints and returns the dom and constraint element
1049 	def getCurrentConstraints(passed_dom=None):
1050 	    """
1051 	    Commandline options:
1052 	      * -f - CIB file, only if passed_dom is None
1053 	    """
1054 	    if passed_dom:
1055 	        dom = passed_dom
1056 	    else:
1057 	        current_constraints_xml = utils.get_cib_xpath("//constraints")
1058 	        if current_constraints_xml == "":
1059 	            utils.err("unable to process cib")
1060 	        # Verify current constraint doesn't already exist
1061 	        # If it does we replace it with the new constraint
CID (unavailable; MK=42e4d47d6c846e21a3e7a16e47445de3) (#1 of 1): XML external entity processing enabled (SIGMA.xml_external_entity_enabled):
(1) Event Sigma main event: The application uses Python's built in `xml` module which does not properly handle erroneous or maliciously constructed data, making the application vulnerable to one or more types of XML attacks.
(2) Event remediation: Avoid using the `xml` module. Consider using the `defusedxml` module or similar which safely prevents all XML entity attacks.
1062 	        dom = parseString(current_constraints_xml)
1063 	
1064 	    constraintsElement = dom.getElementsByTagName("constraints")[0]
1065 	    return (dom, constraintsElement)
1066 	
1067 	
1068 	# If returnStatus is set, then we don't error out, we just print the error
1069 	# and return false
1070 	def constraint_rm(  # noqa: PLR0912
1071 	    lib,
1072 	    argv,
1073 	    modifiers,
1074 	    returnStatus=False,
1075 	    constraintsElement=None,
1076 	    passed_dom=None,
1077 	):
1078 	    """
1079 	    Options:
1080 	      * -f - CIB file, effective only if passed_dom is None
1081 	    """
1082 	    if passed_dom is None:
1083 	        modifiers.ensure_only_supported("-f")
1084 	    if not argv:
1085 	        raise CmdLineInputError()
1086 	
1087 	    bad_constraint = False
1088 	    if len(argv) != 1:
1089 	        for arg in argv:
1090 	            if not constraint_rm(
1091 	                lib, [arg], modifiers, returnStatus=True, passed_dom=passed_dom
1092 	            ):
1093 	                bad_constraint = True
1094 	        if bad_constraint:
1095 	            sys.exit(1)
1096 	        return None
1097 	
1098 	    c_id = argv.pop(0)
1099 	    elementFound = False
1100 	    dom = None
1101 	    use_cibadmin = False
1102 	    if not constraintsElement:
1103 	        (dom, constraintsElement) = getCurrentConstraints(passed_dom)
1104 	        use_cibadmin = True
1105 	
1106 	    for co in constraintsElement.childNodes[:]:
1107 	        if co.nodeType != xml.dom.Node.ELEMENT_NODE:
1108 	            continue
1109 	        if co.getAttribute("id") == c_id:
1110 	            constraintsElement.removeChild(co)
1111 	            elementFound = True
1112 	
1113 	    if not elementFound:
1114 	        for rule in constraintsElement.getElementsByTagName("rule")[:]:
1115 	            if rule.getAttribute("id") == c_id:
1116 	                elementFound = True
1117 	                parent = rule.parentNode
1118 	                parent.removeChild(rule)
1119 	                if not parent.getElementsByTagName("rule"):
1120 	                    parent.parentNode.removeChild(parent)
1121 	
1122 	    if elementFound:
1123 	        if passed_dom:
1124 	            return dom
1125 	        if use_cibadmin:
1126 	            utils.replace_cib_configuration(dom)
1127 	        if returnStatus:
1128 	            return True
1129 	    else:
1130 	        utils.err("Unable to find constraint - '%s'" % c_id, False)
1131 	        if returnStatus:
1132 	            return False
1133 	        sys.exit(1)
1134 	    return None
1135 	
1136 	
1137 	def _split_set_constraints(
1138 	    constraints_dto: CibConstraintsDto,
1139 	) -> tuple[CibConstraintsDto, CibConstraintsDto]:
1140 	    return (
1141 	        CibConstraintsDto(
1142 	            location=constraints_dto.location,
1143 	            colocation=constraints_dto.colocation,
1144 	            order=constraints_dto.order,
1145 	            ticket=constraints_dto.ticket,
1146 	        ),
1147 	        CibConstraintsDto(
1148 	            location_set=constraints_dto.location_set,
1149 	            colocation_set=constraints_dto.colocation_set,
1150 	            order_set=constraints_dto.order_set,
1151 	            ticket_set=constraints_dto.ticket_set,
1152 	        ),
1153 	    )
1154 	
1155 	
1156 	def _find_constraints_containing_resource(
1157 	    resources_dto: CibResourcesDto,
1158 	    constraints_dto: CibConstraintsDto,
1159 	    resource_id: str,
1160 	) -> CibConstraintsDto:
1161 	    resources_filter = [resource_id]
1162 	    # Original implementation only included parent resource only if resource_id
1163 	    # was referring to a primitive resource, ignoring groups. This may change in
1164 	    # the future if necessary.
1165 	    if any(
1166 	        primitive_dto.id == resource_id
1167 	        for primitive_dto in resources_dto.primitives
1168 	    ):
1169 	        for clone_dto in resources_dto.clones:
1170 	            if clone_dto.member_id == resource_id:
1171 	                resources_filter.append(clone_dto.id)
1172 	                break
1173 	    return _filter_constraints_by_resources(
1174 	        constraints_dto, resources_filter, []
1175 	    )
1176 	
1177 	
1178 	def ref(
1179 	    lib: Any, argv: list[str], modifiers: parse_args.InputModifiers
1180 	) -> None:
1181 	    modifiers.ensure_only_supported("-f")
1182 	    if not argv:
1183 	        raise CmdLineInputError()
1184 	
1185 	    resources_dto = cast(
1186 	        CibResourcesDto, lib.resource.get_configured_resources()
1187 	    )
1188 	
1189 	    constraints_dto = cast(
1190 	        CibConstraintsDto,
1191 	        lib.constraint.get_config(evaluate_rules=False),
1192 	    )
1193 	
1194 	    for resource_id in sorted(set(argv)):
1195 	        constraint_ids = get_all_constraints_ids(
1196 	            _find_constraints_containing_resource(
1197 	                resources_dto, constraints_dto, resource_id
1198 	            )
1199 	        )
1200 	        print(f"Resource: {resource_id}")
1201 	        if constraint_ids:
1202 	            print(
1203 	                "\n".join(
1204 	                    indent(
1205 	                        sorted(constraint_ids),
1206 	                        indent_step=INDENT_STEP,
1207 	                    )
1208 	                )
1209 	            )
1210 	        else:
1211 	            print("  No Matches")
1212 	
1213 	
1214 	def remove_constraints_containing(
1215 	    resource_id: str, output=False, constraints_element=None, passed_dom=None
1216 	):
1217 	    """
1218 	    Commandline options:
1219 	      * -f - CIB file, effective only if passed_dom is None
1220 	    """
1221 	    lib = utils.get_library_wrapper()
1222 	    modifiers = utils.get_input_modifiers()
1223 	    resources_dto = cast(
1224 	        CibResourcesDto, lib.resource.get_configured_resources()
1225 	    )
1226 	
1227 	    constraints_dto, set_constraints_dto = _split_set_constraints(
1228 	        cast(
1229 	            CibConstraintsDto,
1230 	            lib.constraint.get_config(evaluate_rules=False),
1231 	        )
1232 	    )
1233 	    constraints = sorted(
1234 	        get_all_constraints_ids(
1235 	            _find_constraints_containing_resource(
1236 	                resources_dto, constraints_dto, resource_id
1237 	            )
1238 	        )
1239 	    )
1240 	    set_constraints = sorted(
1241 	        get_all_constraints_ids(
1242 	            _find_constraints_containing_resource(
1243 	                resources_dto, set_constraints_dto, resource_id
1244 	            )
1245 	        )
1246 	    )
1247 	    for c in constraints:
1248 	        if output:
1249 	            print_to_stderr(f"Removing Constraint - {c}")
1250 	        if constraints_element is not None:
1251 	            constraint_rm(
1252 	                lib,
1253 	                [c],
1254 	                modifiers,
1255 	                True,
1256 	                constraints_element,
1257 	                passed_dom=passed_dom,
1258 	            )
1259 	        else:
1260 	            constraint_rm(lib, [c], modifiers, passed_dom=passed_dom)
1261 	
1262 	    if set_constraints:
1263 	        (dom, constraintsElement) = getCurrentConstraints(passed_dom)
1264 	        for set_c in constraintsElement.getElementsByTagName("resource_ref")[:]:
1265 	            # If resource id is in a set, remove it from the set, if the set
1266 	            # is empty, then we remove the set, if the parent of the set
1267 	            # is empty then we remove it
1268 	            if set_c.getAttribute("id") == resource_id:
1269 	                parent_node = set_c.parentNode
1270 	                parent_node.removeChild(set_c)
1271 	                if output:
1272 	                    print_to_stderr(
1273 	                        "Removing {} from set {}".format(
1274 	                            resource_id, parent_node.getAttribute("id")
1275 	                        )
1276 	                    )
1277 	                if parent_node.getElementsByTagName("resource_ref").length == 0:
1278 	                    print_to_stderr(
1279 	                        "Removing set {}".format(parent_node.getAttribute("id"))
1280 	                    )
1281 	                    parent_node_2 = parent_node.parentNode
1282 	                    parent_node_2.removeChild(parent_node)
1283 	                    if (
1284 	                        parent_node_2.getElementsByTagName(
1285 	                            "resource_set"
1286 	                        ).length
1287 	                        == 0
1288 	                    ):
1289 	                        parent_node_2.parentNode.removeChild(parent_node_2)
1290 	                        print_to_stderr(
1291 	                            "Removing constraint {}".format(
1292 	                                parent_node_2.getAttribute("id")
1293 	                            )
1294 	                        )
1295 	        if passed_dom:
1296 	            return dom
1297 	        utils.replace_cib_configuration(dom)
1298 	    return None
1299 	
1300 	
1301 	# Re-assign any constraints referencing a resource to its parent (a clone
1302 	# or master)
1303 	def constraint_resource_update(old_id, dom):
1304 	    """
1305 	    Commandline options: no options
1306 	    """
1307 	    new_id = None
1308 	    clone_ms_parent = utils.dom_get_resource_clone_ms_parent(dom, old_id)
1309 	    if clone_ms_parent:
1310 	        new_id = clone_ms_parent.getAttribute("id")
1311 	
1312 	    if new_id:
1313 	        constraints = dom.getElementsByTagName("rsc_location")
1314 	        constraints += dom.getElementsByTagName("rsc_order")
1315 	        constraints += dom.getElementsByTagName("rsc_colocation")
1316 	        attrs_to_update = ["rsc", "first", "then", "with-rsc"]
1317 	        for constraint in constraints:
1318 	            for attr in attrs_to_update:
1319 	                if constraint.getAttribute(attr) == old_id:
1320 	                    constraint.setAttribute(attr, new_id)
1321 	    return dom
1322