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