1    	import json
2    	import re
3    	import sys
4    	from collections.abc import Callable, Mapping
5    	from functools import partial
6    	from typing import TYPE_CHECKING, Any
7    	from xml.dom.minidom import parseString
8    	
9    	import pcs.lib.pacemaker.live as lib_pacemaker
10   	import pcs.lib.resource_agent as lib_ra
11   	from pcs import constraint, utils
12   	from pcs.cli.cluster_property.output import PropertyConfigurationFacade
13   	from pcs.cli.common.errors import CmdLineInputError
14   	from pcs.cli.common.output import format_wrap_for_terminal
15   	from pcs.cli.common.parse_args import (
16   	    FUTURE_OPTION,
17   	    OUTPUT_FORMAT_VALUE_CMD,
18   	    OUTPUT_FORMAT_VALUE_JSON,
19   	    OUTPUT_FORMAT_VALUE_TEXT,
20   	    Argv,
21   	    InputModifiers,
22   	    KeyValueParser,
23   	    get_rule_str,
24   	    group_by_keywords,
25   	    wait_to_timeout,
26   	)
27   	from pcs.cli.common.tools import print_to_stderr, timeout_to_seconds_legacy
28   	from pcs.cli.nvset import filter_out_expired_nvset, nvset_dto_list_to_lines
29   	from pcs.cli.reports import process_library_reports
30   	from pcs.cli.reports.output import deprecation_warning, error, warn
31   	from pcs.cli.resource.common import check_is_not_stonith
32   	from pcs.cli.resource.output import (
33   	    operation_defaults_to_cmd,
34   	    resource_agent_metadata_to_text,
35   	    resource_defaults_to_cmd,
36   	)
37   	from pcs.cli.resource.parse_args import (
38   	    parse_bundle_create_options,
39   	    parse_bundle_reset_options,
40   	    parse_bundle_update_options,
41   	    parse_clone,
42   	    parse_create_new,
43   	    parse_create_old,
44   	)
45   	from pcs.cli.resource_agent import find_single_agent
46   	from pcs.common import const, pacemaker, reports
47   	from pcs.common.interface import dto
48   	from pcs.common.pacemaker.defaults import CibDefaultsDto
49   	from pcs.common.pacemaker.resource.operations import (
50   	    OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME,
51   	)
52   	from pcs.lib.cib import const as cib_const
53   	from pcs.lib.cib.resource import guest_node, operations, primitive
54   	from pcs.lib.cib.tools import get_resources
55   	from pcs.lib.commands.resource import (
56   	    _get_nodes_to_validate_against,
57   	    _validate_guest_change,
58   	)
59   	from pcs.lib.errors import LibraryError
60   	from pcs.lib.pacemaker.values import is_true, validate_id
61   	from pcs.settings import (
62   	    pacemaker_wait_timeout_status as PACEMAKER_WAIT_TIMEOUT_STATUS,
63   	)
64   	
65   	if TYPE_CHECKING:
66   	    from pcs.common.resource_agent.dto import ResourceAgentNameDto
67   	
68   	RESOURCE_RELOCATE_CONSTRAINT_PREFIX = "pcs-relocate-"
69   	
70   	
71   	def _detect_guest_change(
72   	    meta_attributes: Mapping[str, str], allow_not_suitable_command: bool
73   	) -> None:
74   	    """
75   	    Commandline options:
76   	      * -f - CIB file
77   	    """
78   	    if not guest_node.is_node_name_in_options(meta_attributes):
79   	        return
80   	
81   	    env = utils.get_lib_env()
82   	    cib = env.get_cib()
83   	    (
84   	        existing_nodes_names,
85   	        existing_nodes_addrs,
86   	        report_list,
87   	    ) = _get_nodes_to_validate_against(env, cib)
88   	    if env.report_processor.report_list(
89   	        report_list
90   	        + _validate_guest_change(
91   	            cib,
92   	            existing_nodes_names,
93   	            existing_nodes_addrs,
94   	            meta_attributes,
95   	            allow_not_suitable_command,
96   	            detect_remove=True,
97   	        )
98   	    ).has_errors:
99   	        raise LibraryError()
100  	
101  	
102  	def resource_utilization_cmd(
103  	    lib: Any, argv: Argv, modifiers: InputModifiers
104  	) -> None:
105  	    """
106  	    Options:
107  	      * -f - CIB file
108  	    """
109  	    modifiers.ensure_only_supported("-f")
110  	    resource_id = None
111  	    if argv:
112  	        resource_id = argv.pop(0)
113  	        check_is_not_stonith(lib, [resource_id])
114  	    utils.print_warning_if_utilization_attrs_has_no_effect(
115  	        PropertyConfigurationFacade.from_properties_dtos(
116  	            lib.cluster_property.get_properties(),
117  	            lib.cluster_property.get_properties_metadata(),
118  	        )
119  	    )
120  	    if not resource_id:
121  	        print_resources_utilization()
122  	        return
123  	    if argv:
124  	        set_resource_utilization(resource_id, argv)
125  	    else:
126  	        print_resource_utilization(resource_id)
127  	
128  	
129  	def _defaults_set_create_cmd(
130  	    lib_command: Callable[..., Any], argv: Argv, modifiers: InputModifiers
131  	) -> None:
132  	    modifiers.ensure_only_supported("-f", "--force")
133  	
134  	    groups = group_by_keywords(
135  	        argv, {"meta", "rule"}, implicit_first_keyword="options"
136  	    )
137  	    groups.ensure_unique_keywords()
138  	    force_flags = set()
139  	    if modifiers.get("--force"):
140  	        force_flags.add(reports.codes.FORCE)
141  	
142  	    lib_command(
143  	        KeyValueParser(groups.get_args_flat("meta")).get_unique(),
144  	        KeyValueParser(groups.get_args_flat("options")).get_unique(),
145  	        nvset_rule=get_rule_str(groups.get_args_flat("rule")),
146  	        force_flags=force_flags,
147  	    )
148  	
149  	
150  	def resource_defaults_set_create_cmd(
151  	    lib: Any, argv: Argv, modifiers: InputModifiers
152  	) -> None:
153  	    """
154  	    Options:
155  	      * -f - CIB file
156  	      * --force - allow unknown options
157  	    """
158  	    return _defaults_set_create_cmd(
159  	        lib.cib_options.resource_defaults_create, argv, modifiers
160  	    )
161  	
162  	
163  	def resource_op_defaults_set_create_cmd(
164  	    lib: Any, argv: Argv, modifiers: InputModifiers
165  	) -> None:
166  	    """
167  	    Options:
168  	      * -f - CIB file
169  	      * --force - allow unknown options
170  	    """
171  	    return _defaults_set_create_cmd(
172  	        lib.cib_options.operation_defaults_create, argv, modifiers
173  	    )
174  	
175  	
176  	def _filter_defaults(
177  	    cib_defaults_dto: CibDefaultsDto, include_expired: bool
178  	) -> CibDefaultsDto:
179  	    return CibDefaultsDto(
180  	        instance_attributes=(
181  	            cib_defaults_dto.instance_attributes
182  	            if include_expired
183  	            else filter_out_expired_nvset(cib_defaults_dto.instance_attributes)
184  	        ),
185  	        meta_attributes=(
186  	            cib_defaults_dto.meta_attributes
187  	            if include_expired
188  	            else filter_out_expired_nvset(cib_defaults_dto.meta_attributes)
189  	        ),
190  	    )
191  	
192  	
193  	def _defaults_config_cmd(
194  	    lib_command: Callable[[bool], CibDefaultsDto],
195  	    defaults_to_cmd: Callable[[CibDefaultsDto], list[list[str]]],
196  	    argv: Argv,
197  	    modifiers: InputModifiers,
198  	) -> None:
199  	    """
200  	    Options:
201  	      * -f - CIB file
202  	      * --all - display all nvsets including the ones with expired rules
203  	      * --full - verbose output
204  	      * --no-expire-check -- disable evaluating whether rules are expired
205  	      * --output-format - supported formats: text, cmd, json
206  	    """
207  	    if argv:
208  	        raise CmdLineInputError()
209  	    modifiers.ensure_only_supported(
210  	        "-f",
211  	        "--all",
212  	        "--full",
213  	        "--no-expire-check",
214  	        output_format_supported=True,
215  	    )
216  	    modifiers.ensure_not_mutually_exclusive("--all", "--no-expire-check")
217  	    output_format = modifiers.get_output_format()
218  	    if (
219  	        modifiers.is_specified("--full")
220  	        and output_format != OUTPUT_FORMAT_VALUE_TEXT
221  	    ):
222  	        raise CmdLineInputError(
223  	            f"option '--full' is not compatible with '{output_format}' output format."
224  	        )
225  	    cib_defaults_dto = _filter_defaults(
226  	        lib_command(not modifiers.get("--no-expire-check")),
227  	        bool(modifiers.get("--all")),
228  	    )
229  	    if output_format == OUTPUT_FORMAT_VALUE_CMD:
230  	        output = ";\n".join(
231  	            " \\\n".join(cmd) for cmd in defaults_to_cmd(cib_defaults_dto)
232  	        )
233  	    elif output_format == OUTPUT_FORMAT_VALUE_JSON:
234  	        output = json.dumps(dto.to_dict(cib_defaults_dto))
235  	    else:
236  	        output = "\n".join(
237  	            nvset_dto_list_to_lines(
238  	                cib_defaults_dto.meta_attributes,
239  	                nvset_label="Meta Attrs",
240  	                with_ids=bool(modifiers.get("--full")),
241  	            )
242  	        )
243  	    if output:
244  	        print(output)
245  	
246  	
247  	def resource_defaults_config_cmd(
248  	    lib: Any, argv: Argv, modifiers: InputModifiers
249  	) -> None:
250  	    """
251  	    Options:
252  	      * -f - CIB file
253  	      * --full - verbose output
254  	    """
255  	    return _defaults_config_cmd(
256  	        lib.cib_options.resource_defaults_config,
257  	        resource_defaults_to_cmd,
258  	        argv,
259  	        modifiers,
260  	    )
261  	
262  	
263  	def resource_op_defaults_config_cmd(
264  	    lib: Any, argv: Argv, modifiers: InputModifiers
265  	) -> None:
266  	    """
267  	    Options:
268  	      * -f - CIB file
269  	      * --full - verbose output
270  	    """
271  	    return _defaults_config_cmd(
272  	        lib.cib_options.operation_defaults_config,
273  	        operation_defaults_to_cmd,
274  	        argv,
275  	        modifiers,
276  	    )
277  	
278  	
279  	def _defaults_set_remove_cmd(
280  	    lib_command: Callable[..., Any], argv: Argv, modifiers: InputModifiers
281  	) -> None:
282  	    """
283  	    Options:
284  	      * -f - CIB file
285  	    """
286  	    modifiers.ensure_only_supported("-f")
287  	    lib_command(argv)
288  	
289  	
290  	def resource_defaults_set_remove_cmd(
291  	    lib: Any, argv: Argv, modifiers: InputModifiers
292  	) -> None:
293  	    """
294  	    Options:
295  	      * -f - CIB file
296  	    """
297  	    return _defaults_set_remove_cmd(
298  	        lib.cib_options.resource_defaults_remove, argv, modifiers
299  	    )
300  	
301  	
302  	def resource_op_defaults_set_remove_cmd(
303  	    lib: Any, argv: Argv, modifiers: InputModifiers
304  	) -> None:
305  	    """
306  	    Options:
307  	      * -f - CIB file
308  	    """
309  	    return _defaults_set_remove_cmd(
310  	        lib.cib_options.operation_defaults_remove, argv, modifiers
311  	    )
312  	
313  	
314  	def _defaults_set_update_cmd(
315  	    lib_command: Callable[..., Any], argv: Argv, modifiers: InputModifiers
316  	) -> None:
317  	    """
318  	    Options:
319  	      * -f - CIB file
320  	    """
321  	    modifiers.ensure_only_supported("-f")
322  	    if not argv:
323  	        raise CmdLineInputError()
324  	
325  	    set_id = argv[0]
326  	    groups = group_by_keywords(argv[1:], {"meta"})
327  	    groups.ensure_unique_keywords()
328  	    lib_command(
329  	        set_id, KeyValueParser(groups.get_args_flat("meta")).get_unique()
330  	    )
331  	
332  	
333  	def resource_defaults_set_update_cmd(
334  	    lib: Any, argv: Argv, modifiers: InputModifiers
335  	) -> None:
336  	    """
337  	    Options:
338  	      * -f - CIB file
339  	    """
340  	    return _defaults_set_update_cmd(
341  	        lib.cib_options.resource_defaults_update, argv, modifiers
342  	    )
343  	
344  	
345  	def resource_op_defaults_set_update_cmd(
346  	    lib: Any, argv: Argv, modifiers: InputModifiers
347  	) -> None:
348  	    """
349  	    Options:
350  	      * -f - CIB file
351  	    """
352  	    return _defaults_set_update_cmd(
353  	        lib.cib_options.operation_defaults_update, argv, modifiers
354  	    )
355  	
356  	
357  	def resource_defaults_update_cmd(
358  	    lib: Any,
359  	    argv: Argv,
360  	    modifiers: InputModifiers,
361  	) -> None:
362  	    """
363  	    Options:
364  	      * -f - CIB file
365  	    """
366  	    del modifiers
367  	    return lib.cib_options.resource_defaults_update(
368  	        None, KeyValueParser(argv).get_unique()
369  	    )
370  	
371  	
372  	def resource_op_defaults_update_cmd(
373  	    lib: Any,
374  	    argv: Argv,
375  	    modifiers: InputModifiers,
376  	) -> None:
377  	    """
378  	    Options:
379  	      * -f - CIB file
380  	    """
381  	    del modifiers
382  	    return lib.cib_options.operation_defaults_update(
383  	        None, KeyValueParser(argv).get_unique()
384  	    )
385  	
386  	
387  	def op_add_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
388  	    """
389  	    Options:
390  	      * -f - CIB file
391  	      * --force - allow unknown options
392  	    """
393  	    if not argv:
394  	        raise CmdLineInputError()
395  	    check_is_not_stonith(lib, [argv[0]], "pcs stonith op add")
396  	    resource_op_add(argv, modifiers)
397  	
398  	
399  	def resource_op_add(argv: Argv, modifiers: InputModifiers) -> None:
400  	    """
401  	    Commandline options:
402  	      * -f - CIB file
403  	      * --force - allow unknown options
404  	    """
405  	    modifiers.ensure_only_supported("-f", "--force")
406  	    if not argv:
407  	        raise CmdLineInputError()
408  	    res_id = argv.pop(0)
409  	
410  	    # Check if we need to upgrade cib schema.
411  	    # To do that, argv must be parsed, which is duplication of parsing in
412  	    # resource_operation_add. But we need to upgrade the cib first before
413  	    # calling that function. Hopefully, this will be fixed in the new pcs
414  	    # architecture.
415  	
416  	    # argv[0] is an operation name
417  	    dom = None
418  	    op_properties = utils.convert_args_to_tuples(argv[1:])
419  	    for key, value in op_properties:
420  	        if key == "on-fail" and value == "demote":
421  	            dom = utils.cluster_upgrade_to_version(
422  	                const.PCMK_ON_FAIL_DEMOTE_CIB_VERSION
423  	            )
424  	            break
425  	    if dom is None:
426  	        dom = utils.get_cib_dom()
427  	
428  	    res_el = utils.dom_get_resource(dom, res_id)
429  	    if not res_el:
430  	        utils.err("Unable to find resource: %s" % res_id)
431  	
432  	    allowed_operation_name_list = None
433  	    agent_name = _get_resource_agent_name_from_rsc_el(res_el)
434  	    try:
435  	        agent_facade = _get_resource_agent_facade(agent_name)
436  	        allowed_operation_name_list = [
437  	            op.name for op in agent_facade.metadata.actions
438  	        ]
439  	    except lib_ra.ResourceAgentError as e:
440  	        # Do not fail with an error to keep backward compatibility.
441  	        # NOTE: Reconsider when moving operation commands to pcs.lib.
442  	        process_library_reports(
443  	            [
444  	                lib_ra.resource_agent_error_to_report_item(
445  	                    e,
446  	                    reports.ReportItemSeverity.warning(),
447  	                )
448  	            ]
449  	        )
450  	
451  	    utils.replace_cib_configuration(
452  	        resource_operation_add(
453  	            dom,
454  	            res_id,
455  	            argv,
456  	            allowed_operation_name_list=allowed_operation_name_list,
457  	        )
458  	    )
459  	
460  	
461  	def op_delete_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
462  	    """
463  	    Options:
464  	      * -f - CIB file
465  	    """
466  	    modifiers.ensure_only_supported("-f")
467  	    if not argv:
468  	        raise CmdLineInputError()
469  	    resource_id = argv.pop(0)
470  	    check_is_not_stonith(lib, [resource_id], "pcs stonith op delete")
471  	    resource_operation_remove(resource_id, argv)
472  	
473  	
474  	def parse_resource_options(
475  	    argv: Argv,
476  	) -> tuple[list[str], list[list[str]], list[str]]:
477  	    """
478  	    Commandline options: no options
479  	    """
480  	    ra_values = []
481  	    op_values: list[list[str]] = []
482  	    meta_values = []
483  	    op_args = False
484  	    meta_args = False
485  	    for arg in argv:
486  	        if arg == "op":
487  	            op_args = True
488  	            meta_args = False
489  	            op_values.append([])
490  	        elif arg == "meta":
491  	            meta_args = True
492  	            op_args = False
493  	        elif op_args:
494  	            if arg == "op":
495  	                op_values.append([])
496  	            elif "=" not in arg and op_values[-1]:
497  	                op_values.append([])
498  	                op_values[-1].append(arg)
499  	            else:
500  	                op_values[-1].append(arg)
501  	        elif meta_args:
502  	            if "=" in arg:
503  	                meta_values.append(arg)
504  	        else:
505  	            ra_values.append(arg)
506  	    return ra_values, op_values, meta_values
507  	
508  	
509  	def resource_list_available(
510  	    lib: Any, argv: Argv, modifiers: InputModifiers
511  	) -> None:
512  	    """
513  	    Options:
514  	      * --nodesc - don't display description
515  	    """
516  	    modifiers.ensure_only_supported("--nodesc")
517  	    if len(argv) > 1:
518  	        raise CmdLineInputError()
519  	
520  	    search = argv[0] if argv else None
521  	    agent_list = lib.resource_agent.list_agents(
522  	        not modifiers.get("--nodesc"), search
523  	    )
524  	
525  	    if not agent_list:
526  	        if search:
527  	            utils.err("No resource agents matching the filter.")
528  	        utils.err(
529  	            "No resource agents available. Do you have resource agents installed?"
530  	        )
531  	
532  	    for agent_info in agent_list:
533  	        name = agent_info["name"]
534  	        shortdesc = agent_info["shortdesc"]
535  	        if shortdesc:
536  	            normalized_desc = " ".join(shortdesc.split())
537  	            print(
538  	                "\n".join(
539  	                    format_wrap_for_terminal(f"{name} - {normalized_desc}")
540  	                )
541  	            )
542  	        else:
543  	            print(name)
544  	
545  	
546  	def resource_list_options(
547  	    lib: Any, argv: Argv, modifiers: InputModifiers
548  	) -> None:
549  	    """
550  	    Options:
551  	      * --full - show advanced
552  	    """
553  	    modifiers.ensure_only_supported("--full")
554  	    if len(argv) != 1:
555  	        raise CmdLineInputError()
556  	
557  	    agent_name_str = argv[0]
558  	    agent_name: ResourceAgentNameDto
559  	    if ":" in agent_name_str:
560  	        agent_name = lib.resource_agent.get_structured_agent_name(
561  	            agent_name_str
562  	        )
563  	    else:
564  	        agent_name = find_single_agent(
565  	            lib.resource_agent.get_agents_list().names, agent_name_str
566  	        )
567  	    if agent_name.standard == "stonith":
568  	        error(
569  	            reports.messages.CommandArgumentTypeMismatch(
570  	                "stonith / fence agents"
571  	            ).message
572  	            + " Please use 'pcs stonith describe' instead."
573  	        )
574  	    print(
575  	        "\n".join(
576  	            resource_agent_metadata_to_text(
577  	                lib.resource_agent.get_agent_metadata(agent_name),
578  	                lib.resource_agent.get_agent_default_operations(
579  	                    agent_name
580  	                ).operations,
581  	                verbose=modifiers.is_specified("--full"),
582  	            )
583  	        )
584  	    )
585  	
586  	
587  	def resource_create(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:  # noqa: PLR0912
588  	    """
589  	    Options:
590  	      * --agent-validation - use agent self validation of instance attributes
591  	      * --before - specified resource inside a group before which new resource
592  	        will be placed inside the group
593  	      * --after - specified resource inside a group after which new resource
594  	        will be placed inside the group
595  	      * --group - specifies group in which resource will be created
596  	      * --force - allow not existing agent, invalid operations or invalid
597  	        instance attributes, allow not suitable command
598  	      * --disabled - created resource will be disabled
599  	      * --no-default-ops - do not add default operations
600  	      * --wait
601  	      * -f - CIB file
602  	      * --future - enable future cli parser behavior
603  	    """
604  	    modifiers_deprecated = ["--before", "--after", "--group"]
605  	    modifiers.ensure_only_supported(
606  	        *(
607  	            [
608  	                "--agent-validation",
609  	                "--force",
610  	                "--disabled",
611  	                "--no-default-ops",
612  	                "--wait",
613  	                "-f",
614  	                FUTURE_OPTION,
615  	            ]
616  	            + ([] if modifiers.get(FUTURE_OPTION) else modifiers_deprecated)
617  	        )
618  	    )
619  	    if len(argv) < 2:
620  	        raise CmdLineInputError()
621  	
622  	    ra_id = argv[0]
623  	    ra_type = argv[1]
624  	
625  	    if modifiers.get(FUTURE_OPTION):
626  	        parts = parse_create_new(argv[2:])
627  	    else:
628  	        parts = parse_create_old(
629  	            argv[2:], modifiers.get_subset(*modifiers_deprecated)
630  	        )
631  	
632  	    defined_options = set()
633  	    if parts.bundle_id:
634  	        defined_options.add("bundle")
635  	    if parts.clone:
636  	        defined_options.add("clone")
637  	    if parts.promotable:
638  	        defined_options.add("promotable")
639  	    if parts.group:
640  	        defined_options.add("group")
641  	    if len(defined_options) > 1:
642  	        raise CmdLineInputError(
643  	            "you can specify only one of clone, promotable, bundle or {}group".format(
644  	                "" if modifiers.get(FUTURE_OPTION) else "--"
645  	            )
646  	        )
647  	
648  	    if parts.group:
649  	        if parts.group.after_resource and parts.group.before_resource:
650  	            raise CmdLineInputError(
651  	                "you cannot specify both 'before' and 'after'"
652  	                if modifiers.get(FUTURE_OPTION)
653  	                else "you cannot specify both --before and --after"
654  	            )
655  	
656  	    if parts.promotable and "promotable" in parts.promotable.meta_attrs:
657  	        raise CmdLineInputError(
658  	            "you cannot specify both promotable option and promotable keyword"
659  	        )
660  	
661  	    settings = dict(
662  	        allow_absent_agent=modifiers.get("--force"),
663  	        allow_invalid_operation=modifiers.get("--force"),
664  	        allow_invalid_instance_attributes=modifiers.get("--force"),
665  	        ensure_disabled=modifiers.get("--disabled"),
666  	        use_default_operations=not modifiers.get("--no-default-ops"),
667  	        wait=modifiers.get("--wait"),
668  	        allow_not_suitable_command=modifiers.get("--force"),
669  	        enable_agent_self_validation=modifiers.get("--agent-validation"),
670  	    )
671  	
672  	    if parts.clone:
673  	        lib.resource.create_as_clone(
674  	            ra_id,
675  	            ra_type,
676  	            parts.primitive.operations,
677  	            parts.primitive.meta_attrs,
678  	            parts.primitive.instance_attrs,
679  	            parts.clone.meta_attrs,
680  	            clone_id=parts.clone.clone_id,
681  	            allow_incompatible_clone_meta_attributes=modifiers.get("--force"),
682  	            **settings,
683  	        )
684  	    elif parts.promotable:
685  	        lib.resource.create_as_clone(
686  	            ra_id,
687  	            ra_type,
688  	            parts.primitive.operations,
689  	            parts.primitive.meta_attrs,
690  	            parts.primitive.instance_attrs,
691  	            dict(**parts.promotable.meta_attrs, promotable="true"),
692  	            clone_id=parts.promotable.clone_id,
693  	            allow_incompatible_clone_meta_attributes=modifiers.get("--force"),
694  	            **settings,
695  	        )
696  	    elif parts.bundle_id:
697  	        settings["allow_not_accessible_resource"] = modifiers.get("--force")
698  	        lib.resource.create_into_bundle(
699  	            ra_id,
700  	            ra_type,
701  	            parts.primitive.operations,
702  	            parts.primitive.meta_attrs,
703  	            parts.primitive.instance_attrs,
704  	            parts.bundle_id,
705  	            **settings,
706  	        )
707  	    elif parts.group:
708  	        adjacent_resource_id = None
709  	        put_after_adjacent = False
710  	        if parts.group.after_resource:
711  	            adjacent_resource_id = parts.group.after_resource
712  	            put_after_adjacent = True
713  	        if parts.group.before_resource:
714  	            adjacent_resource_id = parts.group.before_resource
715  	            put_after_adjacent = False
716  	
717  	        lib.resource.create_in_group(
718  	            ra_id,
719  	            ra_type,
720  	            parts.group.group_id,
721  	            parts.primitive.operations,
722  	            parts.primitive.meta_attrs,
723  	            parts.primitive.instance_attrs,
724  	            adjacent_resource_id=adjacent_resource_id,
725  	            put_after_adjacent=put_after_adjacent,
726  	            **settings,
727  	        )
728  	    else:
729  	        lib.resource.create(
730  	            ra_id,
731  	            ra_type,
732  	            parts.primitive.operations,
733  	            parts.primitive.meta_attrs,
734  	            parts.primitive.instance_attrs,
735  	            **settings,
736  	        )
737  	
738  	
739  	def _parse_resource_move_ban(
740  	    argv: Argv,
741  	) -> tuple[str, str | None, str | None]:
742  	    resource_id = argv.pop(0)
743  	    node = None
744  	    lifetime = None
745  	    while argv:
746  	        arg = argv.pop(0)
747  	        if arg.startswith("lifetime="):
748  	            if lifetime:
749  	                raise CmdLineInputError()
750  	            lifetime = arg.split("=")[1]
751  	            if lifetime and lifetime[0] in list("0123456789"):
752  	                lifetime = "P" + lifetime
753  	        elif not node:
754  	            node = arg
755  	        else:
756  	            raise CmdLineInputError()
757  	    return resource_id, node, lifetime
758  	
759  	
760  	def resource_move_with_constraint(
761  	    lib: Any, argv: Argv, modifiers: InputModifiers
762  	) -> None:
763  	    """
764  	    Options:
765  	      * -f - CIB file
766  	      * --promoted
767  	      * --wait
768  	    """
769  	    modifiers.ensure_only_supported("-f", "--promoted", "--wait")
770  	
771  	    if not argv:
772  	        raise CmdLineInputError("must specify a resource to move")
773  	    if len(argv) > 3:
774  	        raise CmdLineInputError()
775  	    resource_id, node, lifetime = _parse_resource_move_ban(argv)
776  	
777  	    lib.resource.move(
778  	        resource_id,
779  	        node=node,
780  	        master=modifiers.is_specified("--promoted"),
781  	        lifetime=lifetime,
782  	        wait=modifiers.get("--wait"),
783  	    )
784  	
785  	
786  	def resource_move(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
787  	    """
788  	    Options:
789  	      * --promoted
790  	      * --strict
791  	      * --wait
792  	    """
793  	    modifiers.ensure_only_supported(
794  	        "--promoted", "--strict", "--wait", hint_syntax_changed="0.12"
795  	    )
796  	
797  	    if not argv:
798  	        raise CmdLineInputError("must specify a resource to move")
799  	    resource_id = argv.pop(0)
800  	    node = None
801  	    if argv:
802  	        node = argv.pop(0)
803  	    if argv:
804  	        raise CmdLineInputError()
805  	
806  	    lib.resource.move_autoclean(
807  	        resource_id,
808  	        node=node,
809  	        master=modifiers.is_specified("--promoted"),
810  	        wait_timeout=wait_to_timeout(modifiers.get("--wait")),
811  	        strict=modifiers.get("--strict"),
812  	    )
813  	
814  	
815  	def resource_ban(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
816  	    """
817  	    Options:
818  	      * -f - CIB file
819  	      * --promoted
820  	      * --wait
821  	    """
822  	    modifiers.ensure_only_supported("-f", "--promoted", "--wait")
823  	
824  	    if not argv:
825  	        raise CmdLineInputError("must specify a resource to ban")
826  	    if len(argv) > 3:
827  	        raise CmdLineInputError()
828  	    resource_id, node, lifetime = _parse_resource_move_ban(argv)
829  	
830  	    lib.resource.ban(
831  	        resource_id,
832  	        node=node,
833  	        master=modifiers.is_specified("--promoted"),
834  	        lifetime=lifetime,
835  	        wait=modifiers.get("--wait"),
836  	    )
837  	
838  	
839  	def resource_unmove_unban(
840  	    lib: Any, argv: Argv, modifiers: InputModifiers
841  	) -> None:
842  	    """
843  	    Options:
844  	      * -f - CIB file
845  	      * --promoted
846  	      * --wait
847  	    """
848  	    modifiers.ensure_only_supported("-f", "--expired", "--promoted", "--wait")
849  	
850  	    if not argv:
851  	        raise CmdLineInputError("must specify a resource to clear")
852  	    if len(argv) > 2:
853  	        raise CmdLineInputError()
854  	    resource_id = argv.pop(0)
855  	    node = argv.pop(0) if argv else None
856  	
857  	    lib.resource.unmove_unban(
858  	        resource_id,
859  	        node=node,
860  	        master=modifiers.is_specified("--promoted"),
861  	        expired=modifiers.is_specified("--expired"),
862  	        wait=modifiers.get("--wait"),
863  	    )
864  	
865  	
866  	def resource_standards(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
867  	    """
868  	    Options: no options
869  	    """
870  	    modifiers.ensure_only_supported()
871  	    if argv:
872  	        raise CmdLineInputError()
873  	
874  	    standards = lib.resource_agent.list_standards()
875  	
876  	    if standards:
877  	        print("\n".join(standards))
878  	    else:
879  	        utils.err("No standards found")
880  	
881  	
882  	def resource_providers(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
883  	    """
884  	    Options: no options
885  	    """
886  	    modifiers.ensure_only_supported()
887  	    if argv:
888  	        raise CmdLineInputError()
889  	
890  	    providers = lib.resource_agent.list_ocf_providers()
891  	
892  	    if providers:
893  	        print("\n".join(providers))
894  	    else:
895  	        utils.err("No OCF providers found")
896  	
897  	
898  	def resource_agents(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
899  	    """
900  	    Options: no options
901  	    """
902  	    modifiers.ensure_only_supported()
903  	    if len(argv) > 1:
904  	        raise CmdLineInputError()
905  	
906  	    standard = argv[0] if argv else None
907  	
908  	    agents = lib.resource_agent.list_agents_for_standard_and_provider(standard)
909  	
910  	    if agents:
911  	        print("\n".join(agents))
912  	    else:
913  	        utils.err(
914  	            "No agents found{0}".format(
915  	                " for {0}".format(argv[0]) if argv else ""
916  	            )
917  	        )
918  	
919  	
920  	def update_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
921  	    """
922  	    Options:
923  	      * -f - CIB file
924  	      * --agent-validation - use agent self validation of instance attributes
925  	      * --wait
926  	      * --force - allow invalid options, do not fail if not possible to get
927  	        agent metadata, allow not suitable command
928  	    """
929  	    if not argv:
930  	        raise CmdLineInputError()
931  	    check_is_not_stonith(lib, [argv[0]], "pcs stonith update")
932  	    resource_update(argv, modifiers)
933  	
934  	
935  	# Update a resource, removing any args that are empty and adding/updating
936  	# args that are not empty
937  	def resource_update(args: Argv, modifiers: InputModifiers) -> None:  # noqa: PLR0912, PLR0915
938  	    """
939  	    Commandline options:
940  	      * -f - CIB file
941  	      * --agent-validation - use agent self validation of instance attributes
942  	      * --wait
943  	      * --force - allow invalid options, do not fail if not possible to get
944  	        agent metadata, allow not suitable command
945  	    """
946  	    modifiers.ensure_only_supported(
947  	        "-f", "--wait", "--force", "--agent-validation"
948  	    )
949  	    if len(args) < 2:
950  	        raise CmdLineInputError()
951  	    res_id = args.pop(0)
952  	
953  	    # Extract operation arguments
954  	    ra_values, op_values, meta_values = parse_resource_options(args)
955  	
956  	    wait = False
957  	    wait_timeout = None
958  	    if modifiers.is_specified("--wait"):
959  	        # deprecated in the first version of 0.12
960  	        process_library_reports(
961  	            [
962  	                reports.ReportItem.deprecation(
963  	                    reports.messages.ResourceWaitDeprecated()
964  	                )
965  	            ]
966  	        )
967  	
968  	        wait_timeout = utils.validate_wait_get_timeout()
969  	        wait = True
970  	
971  	    # Check if we need to upgrade cib schema.
972  	    # To do that, argv must be parsed, which is duplication of parsing below.
973  	    # But we need to upgrade the cib first before calling that function.
974  	    # Hopefully, this will be fixed in the new pcs architecture.
975  	
976  	    cib_upgraded = False
977  	    for op_argv in op_values:
978  	        if cib_upgraded:
979  	            break
980  	        if len(op_argv) < 2:
981  	            continue
982  	        # argv[0] is an operation name
983  	        op_vars = utils.convert_args_to_tuples(op_argv[1:])
984  	        for key, value in op_vars:
985  	            if key == "on-fail" and value == "demote":
986  	                utils.cluster_upgrade_to_version(
987  	                    const.PCMK_ON_FAIL_DEMOTE_CIB_VERSION
988  	                )
989  	                cib_upgraded = True
990  	                break
991  	
992  	    cib_xml = utils.get_cib()
993  	    dom = utils.get_cib_dom(cib_xml=cib_xml)
994  	
995  	    resource = utils.dom_get_resource(dom, res_id)
996  	    if not resource:
997  	        clone = utils.dom_get_clone(dom, res_id)
998  	        master = utils.dom_get_master(dom, res_id)
999  	        if clone or master:
1000 	            if master:
1001 	                clone = transform_master_to_clone(master)
1002 	            clone_child = utils.dom_elem_get_clone_ms_resource(clone)
1003 	            if clone_child:
1004 	                child_id = clone_child.getAttribute("id")
1005 	                new_args = ["meta"] + ra_values + meta_values
1006 	                for op_args in op_values:
1007 	                    if op_args:
1008 	                        new_args += ["op"] + op_args
1009 	                return resource_update_clone(
1010 	                    dom, clone, child_id, new_args, wait, wait_timeout
1011 	                )
1012 	        utils.err("Unable to find resource: %s" % res_id)
1013 	
1014 	    params = utils.convert_args_to_tuples(ra_values)
1015 	
1016 	    agent_name = _get_resource_agent_name_from_rsc_el(resource)
1017 	    allowed_operation_name_list = None
1018 	    try:
1019 	        agent_facade = _get_resource_agent_facade(agent_name)
1020 	        allowed_operation_name_list = [
1021 	            op.name for op in agent_facade.metadata.actions
1022 	        ]
1023 	        report_list = primitive.validate_resource_instance_attributes_update(
1024 	            utils.cmd_runner(),
1025 	            agent_facade,
1026 	            dict(params),
1027 	            res_id,
1028 	            get_resources(lib_pacemaker.get_cib(cib_xml)),
1029 	            force=bool(modifiers.get("--force")),
1030 	            enable_agent_self_validation=bool(
1031 	                modifiers.get("--agent-validation")
1032 	            ),
1033 	        )
1034 	        if report_list:
1035 	            process_library_reports(report_list)
1036 	    except lib_ra.ResourceAgentError as e:
1037 	        process_library_reports(
1038 	            [
1039 	                lib_ra.resource_agent_error_to_report_item(
1040 	                    e,
1041 	                    reports.get_severity(
1042 	                        reports.codes.FORCE, bool(modifiers.get("--force"))
1043 	                    ),
1044 	                )
1045 	            ]
1046 	        )
1047 	
1048 	    utils.dom_update_instance_attr(resource, params)
1049 	
1050 	    remote_node_name = utils.dom_get_resource_remote_node_name(resource)
1051 	
1052 	    # The "remote-node" meta attribute makes sense (and causes creation of
1053 	    # inner pacemaker resource) only for primitive. The meta attribute
1054 	    # "remote-node" has no special meaning for clone/master. So there is no
1055 	    # need for checking this attribute in clone/master.
1056 	    #
1057 	    # It is ok to not to check it until this point in this function:
1058 	    # 1) Only master/clone element is updated if the parameter "res_id" is an id
1059 	    # of the clone/master element. In that case another function is called and
1060 	    # the code path does not reach this point.
1061 	    # 2) No persistent changes happened until this line if the parameter
1062 	    # "res_id" is an id of the primitive.
1063 	    meta_options = KeyValueParser(meta_values).get_unique()
1064 	    if remote_node_name != guest_node.get_guest_option_value(meta_options):
1065 	        _detect_guest_change(
1066 	            meta_options,
1067 	            bool(modifiers.get("--force")),
1068 	        )
1069 	
1070 	    # TODO: validation should be added after migrating the command to the new
1071 	    # architecture
1072 	    if any(meta_options.values()):
1073 	        command = "stonith" if agent_name.is_stonith else "resource"
1074 	        warn(
1075 	            "Meta attributes are not validated by this command. For "
1076 	            f"validation, please use 'pcs {command} meta' instead."
1077 	        )
1078 	
1079 	    utils.dom_update_meta_attr(
1080 	        resource, utils.convert_args_to_tuples(meta_values)
1081 	    )
1082 	
1083 	    operations_el = resource.getElementsByTagName("operations")
1084 	    if not operations_el:
1085 	        operations_el = dom.createElement("operations")
1086 	        resource.appendChild(operations_el)
1087 	    else:
1088 	        operations_el = operations_el[0]
1089 	
1090 	    get_role = partial(
1091 	        pacemaker.role.get_value_for_cib,
1092 	        is_latest_supported=utils.isCibVersionSatisfied(
1093 	            dom, const.PCMK_NEW_ROLES_CIB_VERSION
1094 	        ),
1095 	    )
1096 	    for op_argv in op_values:
1097 	        if not op_argv:
1098 	            continue
1099 	
1100 	        op_name = op_argv[0]
1101 	        if op_name.find("=") != -1:
1102 	            utils.err(
1103 	                "%s does not appear to be a valid operation action" % op_name
1104 	            )
1105 	
1106 	        if len(op_argv) < 2:
1107 	            continue
1108 	
1109 	        op_role = ""
1110 	        op_vars = utils.convert_args_to_tuples(op_argv[1:])
1111 	
1112 	        for key, value in op_vars:
1113 	            if key == "role":
1114 	                op_role = get_role(value)
1115 	                break
1116 	
1117 	        updating_op = None
1118 	        updating_op_before = None
1119 	        for existing_op in operations_el.getElementsByTagName("op"):
1120 	            if updating_op:
1121 	                updating_op_before = existing_op
1122 	                break
1123 	            existing_op_name = existing_op.getAttribute("name")
1124 	            existing_op_role = get_role(existing_op.getAttribute("role"))
1125 	            if existing_op_role == op_role and existing_op_name == op_name:
1126 	                updating_op = existing_op
1127 	                continue
1128 	
1129 	        if updating_op:
1130 	            updating_op.parentNode.removeChild(updating_op)
1131 	        dom = resource_operation_add(
1132 	            dom,
1133 	            res_id,
1134 	            op_argv,
1135 	            validate_strict=False,
1136 	            before_op=updating_op_before,
1137 	            allowed_operation_name_list=allowed_operation_name_list,
1138 	        )
1139 	
1140 	    utils.replace_cib_configuration(dom)
1141 	
1142 	    if (
1143 	        remote_node_name
1144 	        and remote_node_name
1145 	        != utils.dom_get_resource_remote_node_name(resource)
1146 	    ):
1147 	        # if the resource was a remote node and it is not anymore, (or its name
1148 	        # changed) we need to tell pacemaker about it
1149 	        output, retval = utils.run(
1150 	            ["crm_node", "--force", "--remove", remote_node_name]
1151 	        )
1152 	
1153 	    if modifiers.is_specified("--wait"):
1154 	        args = ["crm_resource", "--wait"]
1155 	        if wait_timeout:
1156 	            args.extend(["--timeout=%s" % wait_timeout])
1157 	        output, retval = utils.run(args)
1158 	        running_on = utils.resource_running_on(res_id)
1159 	        if retval == 0:
1160 	            print_to_stderr(running_on["message"])
1161 	        else:
1162 	            msg = []
1163 	            if retval == PACEMAKER_WAIT_TIMEOUT_STATUS:
1164 	                msg.append("waiting timeout")
1165 	            msg.append(running_on["message"])
1166 	            if retval != 0 and output:
1167 	                msg.append("\n" + output)
1168 	            utils.err("\n".join(msg).strip())
1169 	    return None
1170 	
1171 	
1172 	def resource_update_clone(dom, clone, res_id, args, wait, wait_timeout):
1173 	    """
1174 	    Commandline options:
1175 	      * -f - CIB file
1176 	    """
1177 	    dom, dummy_clone_id = resource_clone_create(
1178 	        dom, [res_id] + args, update_existing=True
1179 	    )
1180 	
1181 	    utils.replace_cib_configuration(dom)
1182 	
1183 	    if wait:
1184 	        args = ["crm_resource", "--wait"]
1185 	        if wait_timeout:
1186 	            args.extend(["--timeout=%s" % wait_timeout])
1187 	        output, retval = utils.run(args)
1188 	        running_on = utils.resource_running_on(clone.getAttribute("id"))
1189 	        if retval == 0:
1190 	            print_to_stderr(running_on["message"])
1191 	        else:
1192 	            msg = []
1193 	            if retval == PACEMAKER_WAIT_TIMEOUT_STATUS:
1194 	                msg.append("waiting timeout")
1195 	            msg.append(running_on["message"])
1196 	            if retval != 0 and output:
1197 	                msg.append("\n" + output)
1198 	            utils.err("\n".join(msg).strip())
1199 	
1200 	    return dom
1201 	
1202 	
1203 	def transform_master_to_clone(master_element):
1204 	    # create a new clone element with the same id
1205 	    dom = master_element.ownerDocument
1206 	    clone_element = dom.createElement("clone")
1207 	    clone_element.setAttribute("id", master_element.getAttribute("id"))
1208 	    # place it next to the master element
1209 	    master_element.parentNode.insertBefore(clone_element, master_element)
1210 	    # move all master's children to the clone
1211 	    while master_element.firstChild:
1212 	        clone_element.appendChild(master_element.firstChild)
1213 	    # remove the master
1214 	    master_element.parentNode.removeChild(master_element)
1215 	    # set meta to make the clone promotable
1216 	    utils.dom_update_meta_attr(clone_element, [("promotable", "true")])
1217 	    return clone_element
1218 	
1219 	
1220 	def resource_operation_add(  # noqa: PLR0912, PLR0915
1221 	    dom,
1222 	    res_id,
1223 	    argv,
1224 	    validate_strict=True,
1225 	    before_op=None,
1226 	    allowed_operation_name_list=None,
1227 	):
1228 	    """
1229 	    Commandline options:
1230 	      * --force
1231 	    """
1232 	    if not argv:
1233 	        raise CmdLineInputError()
1234 	
1235 	    res_el = utils.dom_get_resource(dom, res_id)
1236 	    if not res_el:
1237 	        utils.err("Unable to find resource: %s" % res_id)
1238 	
1239 	    op_name = argv.pop(0)
1240 	    op_properties = utils.convert_args_to_tuples(argv)
1241 	
1242 	    if "=" in op_name:
1243 	        utils.err("%s does not appear to be a valid operation action" % op_name)
1244 	
1245 	    op_dict = dict(op_properties)
1246 	    if "name" in op_dict:
1247 	        # deprecated since pcs-0.12.3
1248 	        deprecation_warning(
1249 	            "Specifying an operation name with 'name=<value>' syntax "
1250 	            "is deprecated and might be removed in a future release. "
1251 	            "Use the operation name as the first argument instead."
1252 	        )
1253 	    else:
1254 	        op_dict["name"] = op_name
1255 	
1256 	    normalized = operations.operations_to_normalized([op_dict])
1257 	    report_list = operations.validate_operation_list(
1258 	        normalized,
1259 	        allowed_operation_name_list,
1260 	        allow_invalid="--force" in utils.pcs_options,
1261 	    )
1262 	    if report_list:
1263 	        process_library_reports(report_list)
1264 	
1265 	    new_role_names_supported = utils.isCibVersionSatisfied(
1266 	        dom, const.PCMK_NEW_ROLES_CIB_VERSION
1267 	    )
1268 	    op_normalized = operations.normalized_to_operations(
1269 	        normalized, new_role_names_supported
1270 	    )[0]
1271 	    op_properties = sorted(op_normalized.items())
1272 	
1273 	    interval = None
1274 	    for key, val in op_properties:
1275 	        if key == "interval":
1276 	            interval = val
1277 	            break
1278 	    if not interval:
1279 	        interval = "60s" if op_name == "monitor" else "0s"
1280 	        op_properties.append(("interval", interval))
1281 	
1282 	    generate_id = True
1283 	    for name, value in op_properties:
1284 	        if name == "id":
1285 	            op_id = value
1286 	            generate_id = False
1287 	            id_valid, id_error = utils.validate_xml_id(value, "operation id")
1288 	            if not id_valid:
1289 	                utils.err(id_error)
1290 	            if utils.does_id_exist(dom, value):
1291 	                utils.err(
1292 	                    "id '%s' is already in use, please specify another one"
1293 	                    % value
1294 	                )
1295 	    if generate_id:
1296 	        op_id = "%s-%s-interval-%s" % (res_id, op_name, interval)
1297 	        op_id = utils.find_unique_id(dom, op_id)
1298 	
1299 	    op_el = dom.createElement("op")
1300 	    op_el.setAttribute("id", op_id)
1301 	    for key, val in op_properties:
1302 	        if key == OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME:
1303 	            attrib_el = dom.createElement("instance_attributes")
1304 	            attrib_el.setAttribute(
1305 	                "id", utils.find_unique_id(dom, "params-" + op_id)
1306 	            )
1307 	            op_el.appendChild(attrib_el)
1308 	            nvpair_el = dom.createElement("nvpair")
1309 	            nvpair_el.setAttribute("name", key)
1310 	            nvpair_el.setAttribute("value", val)
1311 	            nvpair_el.setAttribute(
1312 	                "id", utils.find_unique_id(dom, "-".join((op_id, key, val)))
1313 	            )
1314 	            attrib_el.appendChild(nvpair_el)
1315 	        else:
1316 	            op_el.setAttribute(key, val)
1317 	
1318 	    operations_el = res_el.getElementsByTagName("operations")
1319 	    if not operations_el:
1320 	        operations_el = dom.createElement("operations")
1321 	        res_el.appendChild(operations_el)
1322 	    else:
1323 	        operations_el = operations_el[0]
1324 	        duplicate_op_list = utils.operation_exists(operations_el, op_el)
1325 	        if duplicate_op_list:
1326 	            utils.err(
1327 	                "operation %s with interval %ss already specified for %s:\n%s"
1328 	                % (
1329 	                    op_el.getAttribute("name"),
1330 	                    timeout_to_seconds_legacy(op_el.getAttribute("interval")),
1331 	                    res_id,
1332 	                    "\n".join(
1333 	                        [operation_to_string(op) for op in duplicate_op_list]
1334 	                    ),
1335 	                )
1336 	            )
1337 	        if validate_strict and "--force" not in utils.pcs_options:
1338 	            duplicate_op_list = utils.operation_exists_by_name(
1339 	                operations_el, op_el
1340 	            )
1341 	            if duplicate_op_list:
1342 	                msg = (
1343 	                    "operation {action} already specified for {res}"
1344 	                    + ", use --force to override:\n{op}"
1345 	                )
1346 	                utils.err(
1347 	                    msg.format(
1348 	                        action=op_el.getAttribute("name"),
1349 	                        res=res_id,
1350 	                        op="\n".join(
1351 	                            [
1352 	                                operation_to_string(op)
1353 	                                for op in duplicate_op_list
1354 	                            ]
1355 	                        ),
1356 	                    )
1357 	                )
1358 	
1359 	    operations_el.insertBefore(op_el, before_op)
1360 	    return dom
1361 	
1362 	
1363 	def resource_operation_remove(res_id: str, argv: Argv) -> None:  # noqa: PLR0912
1364 	    """
1365 	    Commandline options:
1366 	      * -f - CIB file
1367 	    """
1368 	    # if no args, then we're removing an operation id
1369 	
1370 	    # Do not ever remove an operations element, even if it is empty. There may
1371 	    # be ACLs set in pacemaker which allow "write" for op elements (adding,
1372 	    # changing and removing) but not operations elements. In such a case,
1373 	    # removing an operations element would cause the whole change to be
1374 	    # rejected by pacemaker with a "permission denied" message.
1375 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
1376 	
1377 	    dom = utils.get_cib_dom()
1378 	    if not argv:
1379 	        for operation in dom.getElementsByTagName("op"):
1380 	            if operation.getAttribute("id") == res_id:
1381 	                parent = operation.parentNode
1382 	                parent.removeChild(operation)
1383 	                utils.replace_cib_configuration(dom)
1384 	                return
1385 	        utils.err("unable to find operation id: %s" % res_id)
1386 	
1387 	    original_argv = " ".join(argv)
1388 	
1389 	    op_name = argv.pop(0)
1390 	    resource_el = None
1391 	
1392 	    for resource in dom.getElementsByTagName("primitive"):
1393 	        if resource.getAttribute("id") == res_id:
1394 	            resource_el = resource
1395 	            break
1396 	
1397 	    if not resource_el:
1398 	        utils.err("Unable to find resource: %s" % res_id)
1399 	        # return to let mypy know that resource_el is not None anymore
1400 	        return
1401 	
1402 	    remove_all = False
1403 	    if not argv:
1404 	        remove_all = True
1405 	
1406 	    op_properties = utils.convert_args_to_tuples(argv)
1407 	    op_properties.append(("name", op_name))
1408 	    found_match = False
1409 	    for op in resource_el.getElementsByTagName("op"):
1410 	        temp_properties = []
1411 	        for attr_name in op.attributes.keys():  # noqa: SIM118, attributes is not a dict
1412 	            if attr_name == "id":
1413 	                continue
1414 	            temp_properties.append(
1415 	                (attr_name, op.attributes.get(attr_name).nodeValue)
1416 	            )
1417 	
1418 	        if remove_all and op.attributes["name"].value == op_name:
1419 	            found_match = True
1420 	            parent = op.parentNode
1421 	            parent.removeChild(op)
1422 	        elif not set(op_properties) ^ set(temp_properties):
1423 	            found_match = True
1424 	            parent = op.parentNode
1425 	            parent.removeChild(op)
1426 	            break
1427 	
1428 	    if not found_match:
1429 	        utils.err("Unable to find operation matching: %s" % original_argv)
1430 	
1431 	    utils.replace_cib_configuration(dom)
1432 	
1433 	
1434 	def resource_group_rm_cmd(
1435 	    lib: Any, argv: Argv, modifiers: InputModifiers
1436 	) -> None:
1437 	    """
1438 	    Options:
1439 	      * --wait
1440 	      * -f - CIB file
1441 	    """
1442 	    del lib
1443 	    modifiers.ensure_only_supported("--wait", "-f")
1444 	    if not argv:
1445 	        raise CmdLineInputError()
1446 	    group_name = argv.pop(0)
1447 	    resource_ids = argv
1448 	
1449 	    cib_dom = resource_group_rm(utils.get_cib_dom(), group_name, resource_ids)
1450 	
1451 	    if modifiers.is_specified("--wait"):
1452 	        # deprecated in the first version of 0.12
1453 	        process_library_reports(
1454 	            [
1455 	                reports.ReportItem.deprecation(
1456 	                    reports.messages.ResourceWaitDeprecated()
1457 	                )
1458 	            ]
1459 	        )
1460 	
1461 	        wait_timeout = utils.validate_wait_get_timeout()
1462 	
1463 	    utils.replace_cib_configuration(cib_dom)
1464 	
1465 	    if modifiers.is_specified("--wait"):
1466 	        args = ["crm_resource", "--wait"]
1467 	        if wait_timeout:
1468 	            args.extend(["--timeout=%s" % wait_timeout])
1469 	        output, retval = utils.run(args)
1470 	        if retval != 0:
1471 	            msg = []
1472 	            if retval == PACEMAKER_WAIT_TIMEOUT_STATUS:
1473 	                msg.append("waiting timeout")
1474 	            if output:
1475 	                msg.append("\n" + output)
1476 	            utils.err("\n".join(msg).strip())
1477 	
1478 	
1479 	def resource_group_add_cmd(
1480 	    lib: Any, argv: Argv, modifiers: InputModifiers
1481 	) -> None:
1482 	    """
1483 	    Options:
1484 	      * --wait
1485 	      * -f - CIB file
1486 	      * --after - place a resource in a group after the specified resource in
1487 	        the group
1488 	      * --before - place a resource in a group before the specified resource in
1489 	        the group
1490 	    """
1491 	    modifiers.ensure_only_supported("--wait", "-f", "--after", "--before")
1492 	    if len(argv) < 2:
1493 	        raise CmdLineInputError()
1494 	
1495 	    group_name = argv.pop(0)
1496 	    resource_names = argv
1497 	    adjacent_name = None
1498 	    after_adjacent = True
1499 	    if modifiers.is_specified("--after") and modifiers.is_specified("--before"):
1500 	        raise CmdLineInputError("you cannot specify both --before and --after")
1501 	    if modifiers.is_specified("--after"):
1502 	        adjacent_name = modifiers.get("--after")
1503 	        after_adjacent = True
1504 	    elif modifiers.is_specified("--before"):
1505 	        adjacent_name = modifiers.get("--before")
1506 	        after_adjacent = False
1507 	
1508 	    lib.resource.group_add(
1509 	        group_name,
1510 	        resource_names,
1511 	        adjacent_resource_id=adjacent_name,
1512 	        put_after_adjacent=after_adjacent,
1513 	        wait=modifiers.get("--wait"),
1514 	    )
1515 	
1516 	
1517 	def resource_clone(
1518 	    lib: Any, argv: Argv, modifiers: InputModifiers, promotable: bool = False
1519 	) -> None:
1520 	    """
1521 	    Options:
1522 	      * --wait
1523 	      * -f - CIB file
1524 	      * --force - allow to clone stonith resource
1525 	    """
1526 	    modifiers.ensure_only_supported("-f", "--force", "--wait")
1527 	    if not argv:
1528 	        raise CmdLineInputError()
1529 	
1530 	    res = argv[0]
1531 	    check_is_not_stonith(lib, [res])
1532 	    cib_dom = utils.get_cib_dom()
1533 	
1534 	    if modifiers.is_specified("--wait"):
1535 	        # deprecated in the first version of 0.12
1536 	        process_library_reports(
1537 	            [
1538 	                reports.ReportItem.deprecation(
1539 	                    reports.messages.ResourceWaitDeprecated()
1540 	                )
1541 	            ]
1542 	        )
1543 	
1544 	        wait_timeout = utils.validate_wait_get_timeout()
1545 	
1546 	    force_flags = set()
1547 	    if modifiers.get("--force"):
1548 	        force_flags.add(reports.codes.FORCE)
1549 	
1550 	    cib_dom, clone_id = resource_clone_create(
1551 	        cib_dom, argv, promotable=promotable, force_flags=force_flags
1552 	    )
1553 	    cib_dom = constraint.constraint_resource_update(res, cib_dom)
1554 	    utils.replace_cib_configuration(cib_dom)
1555 	
1556 	    if modifiers.is_specified("--wait"):
1557 	        args = ["crm_resource", "--wait"]
1558 	        if wait_timeout:
1559 	            args.extend(["--timeout=%s" % wait_timeout])
1560 	        output, retval = utils.run(args)
1561 	        running_on = utils.resource_running_on(clone_id)
1562 	        if retval == 0:
1563 	            print_to_stderr(running_on["message"])
1564 	        else:
1565 	            msg = []
1566 	            if retval == PACEMAKER_WAIT_TIMEOUT_STATUS:
1567 	                msg.append("waiting timeout")
1568 	            msg.append(running_on["message"])
1569 	            if output:
1570 	                msg.append("\n" + output)
1571 	            utils.err("\n".join(msg).strip())
1572 	
1573 	
1574 	def _resource_is_ocf(resource_el) -> bool:
1575 	    return resource_el.getAttribute("class") == "ocf"
1576 	
1577 	
1578 	def _get_resource_agent_name_from_rsc_el(
1579 	    resource_el,
1580 	) -> lib_ra.ResourceAgentName:
1581 	    return lib_ra.ResourceAgentName(
1582 	        resource_el.getAttribute("class"),
1583 	        resource_el.getAttribute("provider"),
1584 	        resource_el.getAttribute("type"),
1585 	    )
1586 	
1587 	
1588 	def _get_resource_agent_facade(
1589 	    resource_agent: lib_ra.ResourceAgentName,
1590 	) -> lib_ra.ResourceAgentFacade:
1591 	    return lib_ra.ResourceAgentFacadeFactory(
1592 	        utils.cmd_runner(), utils.get_report_processor()
1593 	    ).facade_from_parsed_name(resource_agent)
1594 	
1595 	
1596 	def resource_clone_create(  # noqa: PLR0912
1597 	    cib_dom, argv, update_existing=False, promotable=False, force_flags=()
1598 	):
1599 	    """
1600 	    Commandline options:
1601 	      * --force - allow to clone stonith resource
1602 	    """
1603 	    name = argv.pop(0)
1604 	
1605 	    resources_el = cib_dom.getElementsByTagName("resources")[0]
1606 	    element = utils.dom_get_resource(resources_el, name) or utils.dom_get_group(
1607 	        resources_el, name
1608 	    )
1609 	    if not element:
1610 	        utils.err("unable to find group or resource: %s" % name)
1611 	
1612 	    if element.parentNode.tagName == "bundle":
1613 	        utils.err("cannot clone bundle resource")
1614 	
1615 	    if not update_existing:
1616 	        if utils.dom_get_resource_clone(
1617 	            cib_dom, name
1618 	        ) or utils.dom_get_resource_masterslave(cib_dom, name):
1619 	            utils.err("%s is already a clone resource" % name)
1620 	
1621 	        if utils.dom_get_group_clone(
1622 	            cib_dom, name
1623 	        ) or utils.dom_get_group_masterslave(cib_dom, name):
1624 	            utils.err("cannot clone a group that has already been cloned")
1625 	    else:
1626 	        if element.parentNode.tagName != "clone":
1627 	            utils.err("%s is not currently a clone" % name)
1628 	        clone = element.parentNode
1629 	
1630 	    # If element is currently in a group and it's the last member, we get rid
1631 	    # of the group
1632 	    if (
1633 	        element.parentNode.tagName == "group"
1634 	        and element.parentNode.getElementsByTagName("primitive").length <= 1
1635 	    ):
1636 	        element.parentNode.parentNode.removeChild(element.parentNode)
1637 	
1638 	    if element.getAttribute("class") == "stonith":
1639 	        process_library_reports(
1640 	            [
1641 	                reports.ReportItem(
1642 	                    severity=reports.item.get_severity(
1643 	                        reports.codes.FORCE,
1644 	                        is_forced=reports.codes.FORCE in force_flags,
1645 	                    ),
1646 	                    message=reports.messages.CloningStonithResourcesHasNoEffect(
1647 	                        [name]
1648 	                    ),
1649 	                )
1650 	            ]
1651 	        )
1652 	
1653 	    parts = parse_clone(argv, promotable=promotable)
1654 	    _check_clone_incompatible_options_child(
1655 	        element, parts.meta_attrs, force=reports.codes.FORCE in force_flags
1656 	    )
1657 	
1658 	    if not update_existing:
1659 	        clone_id = parts.clone_id
1660 	        if clone_id is not None:
1661 	            report_list = []
1662 	            validate_id(clone_id, reporter=report_list)
1663 	            if report_list:
1664 	                raise CmdLineInputError("invalid id '{}'".format(clone_id))
1665 	            if utils.does_id_exist(cib_dom, clone_id):
1666 	                raise CmdLineInputError(
1667 	                    "id '{}' already exists".format(clone_id),
1668 	                )
1669 	        else:
1670 	            clone_id = utils.find_unique_id(cib_dom, name + "-clone")
1671 	        clone = cib_dom.createElement("clone")
1672 	        clone.setAttribute("id", clone_id)
1673 	        clone.appendChild(element)
1674 	        resources_el.appendChild(clone)
1675 	
1676 	    # TODO: validation should be added after migrating the command to the new
1677 	    # architecture
1678 	    if any(
1679 	        value
1680 	        for name, value in parts.meta_attrs.items()
1681 	        if name != "promotable" or not promotable
1682 	    ):
1683 	        warn(
1684 	            reports.messages.MetaAttrsNotValidatedUnsupportedType(
1685 	                [cib_const.TAG_RESOURCE_CLONE]
1686 	            ).message
1687 	        )
1688 	    utils.dom_update_meta_attr(clone, sorted(parts.meta_attrs.items()))
1689 	
1690 	    return cib_dom, clone.getAttribute("id")
1691 	
1692 	
1693 	def _check_clone_incompatible_options_child(
1694 	    child_el,
1695 	    clone_meta_attrs: Mapping[str, str],
1696 	    force: bool = False,
1697 	):
1698 	    report_list = []
1699 	    if child_el.tagName == "primitive":
1700 	        report_list = _check_clone_incompatible_options_primitive(
1701 	            child_el, clone_meta_attrs, force=force
1702 	        )
1703 	    elif child_el.tagName == "group":
1704 	        group_id = child_el.getAttribute("id")
1705 	        for primitive_el in utils.get_group_children_el_from_el(child_el):
1706 	            report_list.extend(
1707 	                _check_clone_incompatible_options_primitive(
1708 	                    primitive_el,
1709 	                    clone_meta_attrs,
1710 	                    group_id=group_id,
1711 	                    force=force,
1712 	                )
1713 	            )
1714 	    if report_list:
1715 	        process_library_reports(report_list)
1716 	
1717 	
1718 	def _check_clone_incompatible_options_primitive(
1719 	    primitive_el,
1720 	    clone_meta_attrs: Mapping[str, str],
1721 	    group_id: str | None = None,
1722 	    force: bool = False,
1723 	) -> reports.ReportItemList:
1724 	    resource_agent_name = _get_resource_agent_name_from_rsc_el(primitive_el)
1725 	    primitive_id = primitive_el.getAttribute("id")
1726 	    if not _resource_is_ocf(primitive_el):
1727 	        for incompatible_attribute in ("globally-unique", "promotable"):
1728 	            if is_true(clone_meta_attrs.get(incompatible_attribute, "0")):
1729 	                return [
1730 	                    reports.ReportItem.error(
1731 	                        reports.messages.ResourceCloneIncompatibleMetaAttributes(
1732 	                            incompatible_attribute,
1733 	                            resource_agent_name.to_dto(),
1734 	                            resource_id=primitive_id,
1735 	                            group_id=group_id,
1736 	                        )
1737 	                    )
1738 	                ]
1739 	    else:
1740 	        try:
1741 	            resource_agent_facade = _get_resource_agent_facade(
1742 	                resource_agent_name
1743 	            )
1744 	        except lib_ra.ResourceAgentError as e:
1745 	            return [
1746 	                lib_ra.resource_agent_error_to_report_item(
1747 	                    e, reports.get_severity(reports.codes.FORCE, force)
1748 	                )
1749 	            ]
1750 	        if resource_agent_facade.metadata.ocf_version == "1.1" and (
1751 	            is_true(clone_meta_attrs.get("promotable", "0"))
1752 	            and not resource_agent_facade.metadata.provides_promotability
1753 	        ):
1754 	            return [
1755 	                reports.ReportItem(
1756 	                    reports.get_severity(reports.codes.FORCE, force),
1757 	                    reports.messages.ResourceCloneIncompatibleMetaAttributes(
1758 	                        "promotable",
1759 	                        resource_agent_name.to_dto(),
1760 	                        resource_id=primitive_id,
1761 	                        group_id=group_id,
1762 	                    ),
1763 	                )
1764 	            ]
1765 	    return []
1766 	
1767 	
1768 	def resource_clone_master_remove(
1769 	    lib: Any, argv: Argv, modifiers: InputModifiers
1770 	) -> None:
1771 	    """
1772 	    Options:
1773 	      * -f - CIB file
1774 	      * --wait
1775 	    """
1776 	    del lib
1777 	    modifiers.ensure_only_supported("-f", "--wait")
1778 	    if len(argv) != 1:
1779 	        raise CmdLineInputError()
1780 	
1781 	    name = argv.pop()
1782 	    dom = utils.get_cib_dom()
1783 	    resources_el = dom.documentElement.getElementsByTagName("resources")[0]
1784 	
1785 	    # get the resource no matter if user entered a clone or a cloned resource
1786 	    resource = (
1787 	        utils.dom_get_resource(resources_el, name)
1788 	        or utils.dom_get_group(resources_el, name)
1789 	        or utils.dom_get_clone_ms_resource(resources_el, name)
1790 	    )
1791 	    if not resource:
1792 	        utils.err("could not find resource: %s" % name)
1793 	    resource_id = resource.getAttribute("id")
1794 	    clone = utils.dom_get_resource_clone_ms_parent(resources_el, resource_id)
1795 	    if not clone:
1796 	        utils.err("'%s' is not a clone resource" % name)
1797 	
1798 	    if modifiers.is_specified("--wait"):
1799 	        # deprecated in the first version of 0.12
1800 	        process_library_reports(
1801 	            [
1802 	                reports.ReportItem.deprecation(
1803 	                    reports.messages.ResourceWaitDeprecated()
1804 	                )
1805 	            ]
1806 	        )
1807 	
1808 	        wait_timeout = utils.validate_wait_get_timeout()
1809 	
1810 	    # if user requested uncloning a resource contained in a cloned group
1811 	    # remove the resource from the group and leave the clone itself alone
1812 	    # unless the resource is the last one in the group
1813 	    clone_child = utils.dom_get_clone_ms_resource(
1814 	        resources_el, clone.getAttribute("id")
1815 	    )
1816 	    if (
1817 	        clone_child.tagName == "group"
1818 	        and resource.tagName != "group"
1819 	        and len(clone_child.getElementsByTagName("primitive")) > 1
1820 	    ):
1821 	        resource_group_rm(dom, clone_child.getAttribute("id"), [resource_id])
1822 	    else:
1823 	        remove_resource_references(dom, clone.getAttribute("id"))
1824 	        clone.parentNode.appendChild(resource)
1825 	        clone.parentNode.removeChild(clone)
1826 	    utils.replace_cib_configuration(dom)
1827 	
1828 	    if modifiers.is_specified("--wait"):
1829 	        args = ["crm_resource", "--wait"]
1830 	        if wait_timeout:
1831 	            args.extend(["--timeout=%s" % wait_timeout])
1832 	        output, retval = utils.run(args)
1833 	        running_on = utils.resource_running_on(resource_id)
1834 	        if retval == 0:
1835 	            print_to_stderr(running_on["message"])
1836 	        else:
1837 	            msg = []
1838 	            if retval == PACEMAKER_WAIT_TIMEOUT_STATUS:
1839 	                msg.append("waiting timeout")
1840 	            msg.append(running_on["message"])
1841 	            if output:
1842 	                msg.append("\n" + output)
1843 	            utils.err("\n".join(msg).strip())
1844 	
1845 	
1846 	def stonith_level_rm_device(cib_dom, stn_id):
1847 	    """
1848 	    Commandline options: no options
1849 	    """
1850 	    topology_el_list = cib_dom.getElementsByTagName("fencing-topology")
1851 	    if not topology_el_list:
1852 	        return cib_dom
1853 	    topology_el = topology_el_list[0]
1854 	    for level_el in topology_el.getElementsByTagName("fencing-level"):
1855 	        device_list = level_el.getAttribute("devices").split(",")
1856 	        if stn_id in device_list:
1857 	            new_device_list = [dev for dev in device_list if dev != stn_id]
1858 	            if new_device_list:
1859 	                level_el.setAttribute("devices", ",".join(new_device_list))
1860 	            else:
1861 	                level_el.parentNode.removeChild(level_el)
1862 	    if not topology_el.getElementsByTagName("fencing-level"):
1863 	        topology_el.parentNode.removeChild(topology_el)
1864 	    return cib_dom
1865 	
1866 	
1867 	def remove_resource_references(
1868 	    dom, resource_id, output=False, constraints_element=None
1869 	):
1870 	    """
1871 	    Commandline options: no options
1872 	    NOTE: -f - will be used only if dom will be None
1873 	    """
1874 	    for obj_ref in dom.getElementsByTagName("obj_ref"):
1875 	        if obj_ref.getAttribute("id") == resource_id:
1876 	            tag = obj_ref.parentNode
1877 	            tag.removeChild(obj_ref)
1878 	            if tag.getElementsByTagName("obj_ref").length == 0:
1879 	                remove_resource_references(
1880 	                    dom,
1881 	                    tag.getAttribute("id"),
1882 	                    output=output,
1883 	                )
1884 	                tag.parentNode.removeChild(tag)
1885 	    constraint.remove_constraints_containing(
1886 	        resource_id, output, constraints_element, dom
1887 	    )
1888 	    stonith_level_rm_device(dom, resource_id)
1889 	
1890 	    for permission in dom.getElementsByTagName("acl_permission"):
1891 	        if permission.getAttribute("reference") == resource_id:
1892 	            permission.parentNode.removeChild(permission)
1893 	
1894 	    return dom
1895 	
1896 	
1897 	# This removes a resource from a group, but keeps it in the config
1898 	def resource_group_rm(cib_dom, group_name, resource_ids):
1899 	    """
1900 	    Commandline options: no options
1901 	    """
1902 	    dom = cib_dom.getElementsByTagName("configuration")[0]
1903 	
1904 	    all_resources = len(resource_ids) == 0
1905 	
1906 	    group_match = utils.dom_get_group(dom, group_name)
1907 	    if not group_match:
1908 	        utils.err("Group '%s' does not exist" % group_name)
1909 	
1910 	    resources_to_move = []
1911 	    if all_resources:
1912 	        resources_to_move.extend(
1913 	            list(group_match.getElementsByTagName("primitive"))
1914 	        )
1915 	    else:
1916 	        for resource_id in resource_ids:
1917 	            resource = utils.dom_get_resource(group_match, resource_id)
1918 	            if resource:
1919 	                resources_to_move.append(resource)
1920 	            else:
1921 	                utils.err(
1922 	                    "Resource '%s' does not exist in group '%s'"
1923 	                    % (resource_id, group_name)
1924 	                )
1925 	
1926 	    # If the group is in a clone, we don't delete the clone as there may be
1927 	    # constraints associated with it which the user may want to keep. However,
1928 	    # there may be several resources in the group. In that case there is no way
1929 	    # to figure out which one of them should stay in the clone. So we forbid
1930 	    # removing all resources from a cloned group unless there is just one
1931 	    # resource.
1932 	    # This creates an inconsistency:
1933 	    # - consider a cloned group with two resources
1934 	    # - move one resource from the group - it becomes a primitive
1935 	    # - move the last resource from the group - it stays in the clone
1936 	    # So far there has been no request to change this behavior. Unless there is
1937 	    # a request / reason to change it, we'll keep it that way.
1938 	    is_cloned_group = group_match.parentNode.tagName in ["clone", "master"]
1939 	    res_in_group = len(group_match.getElementsByTagName("primitive"))
1940 	    if (
1941 	        is_cloned_group
1942 	        and res_in_group > 1
1943 	        and len(resources_to_move) == res_in_group
1944 	    ):
1945 	        utils.err("Cannot remove all resources from a cloned group")
1946 	    target_node = group_match.parentNode
1947 	    if is_cloned_group and res_in_group > 1:
1948 	        target_node = dom.getElementsByTagName("resources")[0]
1949 	    for resource in resources_to_move:
1950 	        resource.parentNode.removeChild(resource)
1951 	        target_node.appendChild(resource)
1952 	
1953 	    if not group_match.getElementsByTagName("primitive"):
1954 	        group_match.parentNode.removeChild(group_match)
1955 	        remove_resource_references(dom, group_name, output=True)
1956 	
1957 	    return cib_dom
1958 	
1959 	
1960 	def resource_group_list(
1961 	    lib: Any, argv: Argv, modifiers: InputModifiers
1962 	) -> None:
1963 	    """
1964 	    Options:
1965 	      * -f - CIB file
1966 	    """
1967 	    del lib
1968 	    modifiers.ensure_only_supported("-f")
1969 	    if argv:
1970 	        raise CmdLineInputError()
1971 	    group_xpath = "//group"
1972 	    group_xml = utils.get_cib_xpath(group_xpath)
1973 	
1974 	    # If no groups exist, we silently return
1975 	    if group_xml == "":
1976 	        return
1977 	
CID (unavailable; MK=be579b78ae5a93e59e5244e356de61d7) (#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.
1978 	    element = parseString(group_xml).documentElement
1979 	    # If there is more than one group returned it's wrapped in an xpath-query
1980 	    # element
1981 	    # Ignoring mypy errors in this very old code. So far, nobody reported a bug
1982 	    # related to these lines.
1983 	    if element.tagName == "xpath-query":  # type: ignore
1984 	        elements = element.getElementsByTagName("group")  # type: ignore
1985 	    else:
1986 	        elements = [element]
1987 	
1988 	    for e in elements:
1989 	        line_parts = [e.getAttribute("id") + ":"]
1990 	        line_parts.extend(
1991 	            resource.getAttribute("id")
1992 	            for resource in e.getElementsByTagName("primitive")
1993 	        )
1994 	        print(" ".join(line_parts))
1995 	
1996 	
1997 	def resource_status(  # noqa: PLR0912, PLR0915
1998 	    lib: Any, argv: Argv, modifiers: InputModifiers, stonith: bool = False
1999 	) -> None:
2000 	    """
2001 	    Options:
2002 	      * -f - CIB file
2003 	      * --hide-inactive - print only active resources
2004 	    """
2005 	    del lib
2006 	    modifiers.ensure_only_supported("-f", "--hide-inactive")
2007 	    if len(argv) > 2:
2008 	        raise CmdLineInputError()
2009 	
2010 	    monitor_command = ["crm_mon", "--one-shot"]
2011 	    if not modifiers.get("--hide-inactive"):
2012 	        monitor_command.append("--inactive")
2013 	
2014 	    resource_or_tag_id = None
2015 	    node = None
2016 	    crm_mon_err_msg = "unable to get cluster status from crm_mon\n"
2017 	    if argv:
2018 	        for arg in argv[:]:
2019 	            if "=" not in arg:
2020 	                resource_or_tag_id = arg
2021 	                crm_mon_err_msg = f"unable to get status of '{resource_or_tag_id}' from crm_mon\n"
2022 	                monitor_command.extend(
2023 	                    [
2024 	                        "--include",
2025 	                        "none,resources",
2026 	                        "--resource",
2027 	                        resource_or_tag_id,
2028 	                    ]
2029 	                )
2030 	                argv.remove(arg)
2031 	                break
2032 	        parser = KeyValueParser(argv)
2033 	        parser.check_allowed_keys({"node"})
2034 	        node = parser.get_unique().get("node")
2035 	        if node == "":
2036 	            utils.err("missing value of 'node' option")
2037 	        if node:
2038 	            monitor_command.extend(["--node", node])
2039 	
2040 	    output, retval = utils.run(monitor_command)
2041 	    if retval != 0:
2042 	        utils.err(crm_mon_err_msg + output.rstrip())
2043 	    preg = re.compile(r".*(stonith:.*)")
2044 	    resources_header = False
2045 	    in_resources = False
2046 	    has_resources = False
2047 	    no_resources_line = (
2048 	        "NO stonith devices configured"
2049 	        if stonith
2050 	        else "NO resources configured"
2051 	    )
2052 	    no_active_resources_msg = "No active resources"
2053 	    for line in output.split("\n"):
2054 	        if line in (
2055 	            "  * No active resources",  # pacemaker >= 2.0.3 with --hide-inactive
2056 	            "No active resources",  # pacemaker < 2.0.3 with --hide-inactive
2057 	        ):
2058 	            print(no_active_resources_msg)
2059 	            return
2060 	        if line in (
2061 	            "  * No resources",  # pacemaker >= 2.0.3
2062 	            "No resources",  # pacemaker < 2.0.3
2063 	        ):
2064 	            if resource_or_tag_id and not node:
2065 	                utils.err(
2066 	                    f"resource or tag id '{resource_or_tag_id}' not found"
2067 	                )
2068 	            if not node:
2069 	                print(no_resources_line)
2070 	            else:
2071 	                print(no_active_resources_msg)
2072 	            return
2073 	        if line in (
2074 	            "Full List of Resources:",  # pacemaker >= 2.0.3
2075 	            "Active Resources:",  # pacemaker >= 2.0.3 with  --hide-inactive
2076 	        ):
2077 	            in_resources = True
2078 	            continue
2079 	        if line in (
2080 	            "Full list of resources:",  # pacemaker < 2.0.3
2081 	            "Active resources:",  # pacemaker < 2.0.3 with --hide-inactive
2082 	        ):
2083 	            resources_header = True
2084 	            continue
2085 	        if line == "":
2086 	            if resources_header:
2087 	                resources_header = False
2088 	                in_resources = True
2089 	            elif in_resources:
2090 	                if not has_resources:
2091 	                    print(no_resources_line)
2092 	                return
2093 	            continue
2094 	        if in_resources:
2095 	            if resource_or_tag_id:
2096 	                has_resources = True
2097 	                print(line)
2098 	                continue
2099 	            if (
2100 	                not preg.match(line)
2101 	                and not stonith
2102 	                or preg.match(line)
2103 	                and stonith
2104 	            ):
2105 	                has_resources = True
2106 	                print(line)
2107 	
2108 	
2109 	def resource_disable_cmd(
2110 	    lib: Any, argv: Argv, modifiers: InputModifiers
2111 	) -> None:
2112 	    """
2113 	    Options:
2114 	      * -f - CIB file
2115 	      * --brief - show brief output of --simulate
2116 	      * --safe - only disable if no other resource gets stopped or demoted
2117 	      * --simulate - do not push the CIB, print its effects
2118 	      * --no-strict - allow disable if other resource is affected
2119 	      * --wait
2120 	    """
2121 	    if not argv:
2122 	        raise CmdLineInputError("You must specify resource(s) to disable")
2123 	    check_is_not_stonith(lib, argv, "pcs stonith disable")
2124 	    resource_disable_common(lib, argv, modifiers)
2125 	
2126 	
2127 	def resource_disable_common(
2128 	    lib: Any, argv: Argv, modifiers: InputModifiers
2129 	) -> None:
2130 	    """
2131 	    Commandline options:
2132 	      * -f - CIB file
2133 	      * --force - allow to disable the last stonith resource in the cluster
2134 	      * --brief - show brief output of --simulate
2135 	      * --safe - only disable if no other resource gets stopped or demoted
2136 	      * --simulate - do not push the CIB, print its effects
2137 	      * --no-strict - allow disable if other resource is affected
2138 	      * --wait
2139 	    """
2140 	    modifiers.ensure_only_supported(
2141 	        "-f",
2142 	        "--force",
2143 	        "--brief",
2144 	        "--safe",
2145 	        "--simulate",
2146 	        "--no-strict",
2147 	        "--wait",
2148 	    )
2149 	    modifiers.ensure_not_mutually_exclusive("-f", "--simulate", "--wait")
2150 	    modifiers.ensure_not_incompatible("--simulate", {"-f", "--safe", "--wait"})
2151 	    modifiers.ensure_not_incompatible("--safe", {"-f", "--simulate"})
2152 	    modifiers.ensure_not_incompatible("--no-strict", {"-f"})
2153 	
2154 	    if not argv:
2155 	        raise CmdLineInputError("You must specify resource(s) to disable")
2156 	
2157 	    if modifiers.get("--simulate"):
2158 	        result = lib.resource.disable_simulate(
2159 	            argv, not modifiers.get("--no-strict")
2160 	        )
2161 	        if modifiers.get("--brief"):
2162 	            # if the result is empty, printing it would produce a new line,
2163 	            # which is not wanted
2164 	            if result["other_affected_resource_list"]:
2165 	                print("\n".join(result["other_affected_resource_list"]))
2166 	            return
2167 	        print(result["plaintext_simulated_status"])
2168 	        return
2169 	    if modifiers.get("--safe") or modifiers.get("--no-strict"):
2170 	        if modifiers.get("--brief"):
2171 	            # Brief mode skips simulation output by setting the report processor
2172 	            # to ignore info reports which contain crm_simulate output and
2173 	            # resource status in this command
2174 	            lib.env.report_processor.suppress_reports_of_severity(
2175 	                [reports.ReportItemSeverity.INFO]
2176 	            )
2177 	        lib.resource.disable_safe(
2178 	            argv,
2179 	            not modifiers.get("--no-strict"),
2180 	            modifiers.get("--wait"),
2181 	        )
2182 	        return
2183 	    if modifiers.get("--brief"):
2184 	        raise CmdLineInputError(
2185 	            "'--brief' cannot be used without '--simulate' or '--safe'"
2186 	        )
2187 	    force_flags = set()
2188 	    if modifiers.get("--force"):
2189 	        force_flags.add(reports.codes.FORCE)
2190 	    lib.resource.disable(argv, modifiers.get("--wait"), force_flags)
2191 	
2192 	
2193 	def resource_safe_disable_cmd(
2194 	    lib: Any, argv: Argv, modifiers: InputModifiers
2195 	) -> None:
2196 	    """
2197 	    Options:
2198 	      * --brief - show brief output of --simulate
2199 	      * --force - skip checks for safe resource disable
2200 	      * --no-strict - allow disable if other resource is affected
2201 	      * --simulate - do not push the CIB, print its effects
2202 	      * --wait
2203 	    """
2204 	    modifiers.ensure_only_supported(
2205 	        "--brief", "--force", "--no-strict", "--simulate", "--wait"
2206 	    )
2207 	    modifiers.ensure_not_incompatible("--force", {"--no-strict", "--simulate"})
2208 	    custom_options = {}
2209 	    if modifiers.get("--force"):
2210 	        warn(
2211 	            "option '--force' is specified therefore checks for disabling "
2212 	            "resource safely will be skipped"
2213 	        )
2214 	    elif not modifiers.get("--simulate"):
2215 	        custom_options["--safe"] = True
2216 	    resource_disable_cmd(
2217 	        lib,
2218 	        argv,
2219 	        modifiers.get_subset(
2220 	            "--wait", "--no-strict", "--simulate", "--brief", **custom_options
2221 	        ),
2222 	    )
2223 	
2224 	
2225 	def resource_enable_cmd(
2226 	    lib: Any, argv: Argv, modifiers: InputModifiers
2227 	) -> None:
2228 	    """
2229 	    Options:
2230 	      * --wait
2231 	      * -f - CIB file
2232 	    """
2233 	    modifiers.ensure_only_supported("--wait", "-f")
2234 	    if not argv:
2235 	        raise CmdLineInputError("You must specify resource(s) to enable")
2236 	    resources = argv
2237 	    check_is_not_stonith(lib, resources, "pcs stonith enable")
2238 	    lib.resource.enable(resources, modifiers.get("--wait"))
2239 	
2240 	
2241 	def resource_restart_cmd(
2242 	    lib: Any, argv: Argv, modifiers: InputModifiers
2243 	) -> None:
2244 	    """
2245 	    Options:
2246 	      * --wait
2247 	    """
2248 	    modifiers.ensure_only_supported("--wait")
2249 	
2250 	    if not argv:
2251 	        raise CmdLineInputError(
2252 	            "You must specify a resource to restart",
2253 	            show_both_usage_and_message=True,
2254 	        )
2255 	    resource = argv.pop(0)
2256 	    node = argv.pop(0) if argv else None
2257 	    if argv:
2258 	        raise CmdLineInputError()
2259 	
2260 	    timeout = (
2261 	        modifiers.get("--wait") if modifiers.is_specified("--wait") else None
2262 	    )
2263 	
2264 	    lib.resource.restart(resource, node, timeout)
2265 	
2266 	    print_to_stderr(f"{resource} successfully restarted")
2267 	
2268 	
2269 	def resource_force_action(  # noqa: PLR0912
2270 	    lib: Any, argv: Argv, modifiers: InputModifiers, action: str
2271 	) -> None:
2272 	    """
2273 	    Options:
2274 	      * --force
2275 	      * --full - more verbose output
2276 	    """
2277 	    modifiers.ensure_only_supported("--force", "--full")
2278 	    action_command = {
2279 	        "debug-start": "--force-start",
2280 	        "debug-stop": "--force-stop",
2281 	        "debug-promote": "--force-promote",
2282 	        "debug-demote": "--force-demote",
2283 	        "debug-monitor": "--force-check",
2284 	    }
2285 	
2286 	    if action not in action_command:
2287 	        raise CmdLineInputError()
2288 	    if not argv:
2289 	        utils.err("You must specify a resource to {0}".format(action))
2290 	    if len(argv) != 1:
2291 	        raise CmdLineInputError()
2292 	
2293 	    resource = argv[0]
2294 	    check_is_not_stonith(lib, [resource])
2295 	    dom = utils.get_cib_dom()
2296 	
2297 	    if not (
2298 	        utils.dom_get_any_resource(dom, resource)
2299 	        or utils.dom_get_bundle(dom, resource)
2300 	    ):
2301 	        utils.err(
2302 	            "unable to find a resource/clone/group/bundle: {0}".format(resource)
2303 	        )
2304 	    bundle_el = utils.dom_get_bundle(dom, resource)
2305 	    if bundle_el:
2306 	        bundle_resource = utils.dom_get_resource_bundle(bundle_el)
2307 	        if bundle_resource:
2308 	            utils.err(
2309 	                "unable to {0} a bundle, try the bundle's resource: {1}".format(
2310 	                    action, bundle_resource.getAttribute("id")
2311 	                )
2312 	            )
2313 	        else:
2314 	            utils.err("unable to {0} a bundle".format(action))
2315 	    if utils.dom_get_group(dom, resource):
2316 	        group_resources = utils.get_group_children(resource)
2317 	        utils.err(
2318 	            (
2319 	                "unable to {0} a group, try one of the group's resource(s) ({1})"
2320 	            ).format(action, ",".join(group_resources))
2321 	        )
2322 	    if utils.dom_get_clone(dom, resource) or utils.dom_get_master(
2323 	        dom, resource
2324 	    ):
2325 	        clone_resource = utils.dom_get_clone_ms_resource(dom, resource)
2326 	        utils.err(
2327 	            "unable to {0} a clone, try the clone's resource: {1}".format(
2328 	                action, clone_resource.getAttribute("id")
2329 	            )
2330 	        )
2331 	
2332 	    args = ["crm_resource", "-r", resource, action_command[action]]
2333 	    if modifiers.get("--full"):
2334 	        # set --verbose twice to get a reasonable amount of debug messages
2335 	        args.extend(["--verbose"] * 2)
2336 	    if modifiers.get("--force"):
2337 	        args.append("--force")
2338 	    output, retval = utils.run(args)
2339 	
2340 	    if "doesn't support group resources" in output:
2341 	        utils.err("groups are not supported")
2342 	        sys.exit(retval)
2343 	    if "doesn't support stonith resources" in output:
2344 	        utils.err("stonith devices are not supported")
2345 	        sys.exit(retval)
2346 	
2347 	    print(output.rstrip())
2348 	    sys.exit(retval)
2349 	
2350 	
2351 	def resource_manage_cmd(
2352 	    lib: Any, argv: Argv, modifiers: InputModifiers
2353 	) -> None:
2354 	    """
2355 	    Options:
2356 	      * -f - CIB file
2357 	      * --monitor - enable monitor operation of specified resources
2358 	    """
2359 	    modifiers.ensure_only_supported("-f", "--monitor")
2360 	    if not argv:
2361 	        raise CmdLineInputError("You must specify resource(s) to manage")
2362 	    resources = argv
2363 	    check_is_not_stonith(lib, resources)
2364 	    lib.resource.manage(resources, with_monitor=modifiers.get("--monitor"))
2365 	
2366 	
2367 	def resource_unmanage_cmd(
2368 	    lib: Any, argv: Argv, modifiers: InputModifiers
2369 	) -> None:
2370 	    """
2371 	    Options:
2372 	      * -f - CIB file
2373 	      * --monitor - bisable monitor operation of specified resources
2374 	    """
2375 	    modifiers.ensure_only_supported("-f", "--monitor")
2376 	    if not argv:
2377 	        raise CmdLineInputError("You must specify resource(s) to unmanage")
2378 	    resources = argv
2379 	    check_is_not_stonith(lib, resources)
2380 	    lib.resource.unmanage(resources, with_monitor=modifiers.get("--monitor"))
2381 	
2382 	
2383 	def resource_failcount_show(
2384 	    lib: Any, argv: Argv, modifiers: InputModifiers
2385 	) -> None:
2386 	    """
2387 	    Options:
2388 	      * --full
2389 	      * -f - CIB file
2390 	    """
2391 	    modifiers.ensure_only_supported("-f", "--full")
2392 	
2393 	    resource = argv.pop(0) if argv and "=" not in argv[0] else None
2394 	    parser = KeyValueParser(argv)
2395 	    parser.check_allowed_keys({"node", "operation", "interval"})
2396 	    parsed_options = parser.get_unique()
2397 	
2398 	    node = parsed_options.get("node")
2399 	    operation = parsed_options.get("operation")
2400 	    interval = parsed_options.get("interval")
2401 	    result_lines = []
2402 	    failures_data = lib.resource.get_failcounts(
2403 	        resource=resource, node=node, operation=operation, interval=interval
2404 	    )
2405 	
2406 	    if not failures_data:
2407 	        result_lines.append(
2408 	            __headline_resource_failures(
2409 	                True, resource, node, operation, interval
2410 	            )
2411 	        )
2412 	        print("\n".join(result_lines))
2413 	        return
2414 	
2415 	    resource_list = sorted({fail["resource"] for fail in failures_data})
2416 	    for current_resource in resource_list:
2417 	        result_lines.append(
2418 	            __headline_resource_failures(
2419 	                False, current_resource, node, operation, interval
2420 	            )
2421 	        )
2422 	        resource_failures = [
2423 	            fail
2424 	            for fail in failures_data
2425 	            if fail["resource"] == current_resource
2426 	        ]
2427 	        node_list = sorted({fail["node"] for fail in resource_failures})
2428 	        for current_node in node_list:
2429 	            node_failures = [
2430 	                fail
2431 	                for fail in resource_failures
2432 	                if fail["node"] == current_node
2433 	            ]
2434 	            if modifiers.get("--full"):
2435 	                result_lines.append(f"  {current_node}:")
2436 	                operation_list = sorted(
2437 	                    {fail["operation"] for fail in node_failures}
2438 	                )
2439 	                for current_operation in operation_list:
2440 	                    operation_failures = [
2441 	                        fail
2442 	                        for fail in node_failures
2443 	                        if fail["operation"] == current_operation
2444 	                    ]
2445 	                    interval_list = sorted(
2446 	                        {fail["interval"] for fail in operation_failures},
2447 	                        # pacemaker's definition of infinity
2448 	                        key=lambda x: 1000000 if x == "INFINITY" else x,
2449 	                    )
2450 	                    for current_interval in interval_list:
2451 	                        interval_failures = [
2452 	                            fail
2453 	                            for fail in operation_failures
2454 	                            if fail["interval"] == current_interval
2455 	                        ]
2456 	                        failcount, dummy_last_failure = __aggregate_failures(
2457 	                            interval_failures
2458 	                        )
2459 	                        result_lines.append(
2460 	                            f"    {current_operation} {current_interval}ms: {failcount}"
2461 	                        )
2462 	            else:
2463 	                failcount, dummy_last_failure = __aggregate_failures(
2464 	                    node_failures
2465 	                )
2466 	                result_lines.append(f"  {current_node}: {failcount}")
2467 	    print("\n".join(result_lines))
2468 	
2469 	
2470 	def __aggregate_failures(failure_list):
2471 	    """
2472 	    Commandline options: no options
2473 	    """
2474 	    last_failure = 0
2475 	    fail_count = 0
2476 	    for failure in failure_list:
2477 	        # infinity is a maximal value and cannot be increased
2478 	        if fail_count != "INFINITY":
2479 	            if failure["fail_count"] == "INFINITY":
2480 	                fail_count = failure["fail_count"]
2481 	            else:
2482 	                fail_count += failure["fail_count"]
2483 	        last_failure = max(last_failure, failure["last_failure"])
2484 	    return fail_count, last_failure
2485 	
2486 	
2487 	def __headline_resource_failures(empty, resource, node, operation, interval):
2488 	    """
2489 	    Commandline options: no options
2490 	    """
2491 	    headline_parts = []
2492 	    if empty:
2493 	        headline_parts.append("No failcounts")
2494 	    else:
2495 	        headline_parts.append("Failcounts")
2496 	    if operation:
2497 	        headline_parts.append("for operation '{operation}'")
2498 	        if interval:
2499 	            headline_parts.append("with interval '{interval}'")
2500 	    if resource:
2501 	        headline_parts.append("of" if operation else "for")
2502 	        headline_parts.append("resource '{resource}'")
2503 	    if node:
2504 	        headline_parts.append("on node '{node}'")
2505 	    return " ".join(headline_parts).format(
2506 	        node=node, resource=resource, operation=operation, interval=interval
2507 	    )
2508 	
2509 	
2510 	def operation_to_string(op_el):
2511 	    """
2512 	    Commandline options: no options
2513 	    """
2514 	    parts = []
2515 	    parts.append(op_el.getAttribute("name"))
2516 	    for name, value in sorted(op_el.attributes.items()):
2517 	        if name in ["id", "name"]:
2518 	            continue
2519 	        parts.append(name + "=" + value)
2520 	    parts.extend(
2521 	        f"{nvpair.getAttribute('name')}={nvpair.getAttribute('value')}"
2522 	        for nvpair in op_el.getElementsByTagName("nvpair")
2523 	    )
2524 	    parts.append("(" + op_el.getAttribute("id") + ")")
2525 	    return " ".join(parts)
2526 	
2527 	
2528 	def resource_cleanup(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2529 	    """
2530 	    Options: no options
2531 	    """
2532 	    del lib
2533 	    modifiers.ensure_only_supported("--strict")
2534 	    resource = argv.pop(0) if argv and "=" not in argv[0] else None
2535 	    parser = KeyValueParser(argv)
2536 	    parser.check_allowed_keys({"node", "operation", "interval"})
2537 	    parsed_options = parser.get_unique()
2538 	
2539 	    print_to_stderr(
2540 	        lib_pacemaker.resource_cleanup(
2541 	            utils.cmd_runner(),
2542 	            resource=resource,
2543 	            node=parsed_options.get("node"),
2544 	            operation=parsed_options.get("operation"),
2545 	            interval=parsed_options.get("interval"),
2546 	            strict=bool(modifiers.get("--strict")),
2547 	        )
2548 	    )
2549 	
2550 	
2551 	def resource_refresh(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2552 	    """
2553 	    Options:
2554 	      * --force - do refresh even though it may be time consuming
2555 	    """
2556 	    del lib
2557 	    modifiers.ensure_only_supported(
2558 	        "--force",
2559 	        "--strict",
2560 	        hint_syntax_changed=(
2561 	            "0.11" if modifiers.is_specified("--full") else None
2562 	        ),
2563 	    )
2564 	    resource = argv.pop(0) if argv and "=" not in argv[0] else None
2565 	    parser = KeyValueParser(argv)
2566 	    parser.check_allowed_keys({"node"})
2567 	    parsed_options = parser.get_unique()
2568 	    print_to_stderr(
2569 	        lib_pacemaker.resource_refresh(
2570 	            utils.cmd_runner(),
2571 	            resource=resource,
2572 	            node=parsed_options.get("node"),
2573 	            strict=bool(modifiers.get("--strict")),
2574 	            force=bool(modifiers.get("--force")),
2575 	        )
2576 	    )
2577 	
2578 	
2579 	def resource_relocate_show_cmd(
2580 	    lib: Any, argv: Argv, modifiers: InputModifiers
2581 	) -> None:
2582 	    """
2583 	    Options: no options
2584 	    """
2585 	    del lib
2586 	    modifiers.ensure_only_supported()
2587 	    if argv:
2588 	        raise CmdLineInputError()
2589 	    resource_relocate_show(utils.get_cib_dom())
2590 	
2591 	
2592 	def resource_relocate_dry_run_cmd(
2593 	    lib: Any, argv: Argv, modifiers: InputModifiers
2594 	) -> None:
2595 	    """
2596 	    Options:
2597 	      * -f - CIB file
2598 	    """
2599 	    modifiers.ensure_only_supported("-f")
2600 	    if argv:
2601 	        check_is_not_stonith(lib, argv)
2602 	    resource_relocate_run(utils.get_cib_dom(), argv, dry=True)
2603 	
2604 	
2605 	def resource_relocate_run_cmd(
2606 	    lib: Any, argv: Argv, modifiers: InputModifiers
2607 	) -> None:
2608 	    """
2609 	    Options: no options
2610 	    """
2611 	    modifiers.ensure_only_supported()
2612 	    if argv:
2613 	        check_is_not_stonith(lib, argv)
2614 	    resource_relocate_run(utils.get_cib_dom(), argv, dry=False)
2615 	
2616 	
2617 	def resource_relocate_clear_cmd(
2618 	    lib: Any, argv: Argv, modifiers: InputModifiers
2619 	) -> None:
2620 	    """
2621 	    Options:
2622 	      * -f - CIB file
2623 	    """
2624 	    del lib
2625 	    modifiers.ensure_only_supported("-f")
2626 	    if argv:
2627 	        raise CmdLineInputError()
2628 	    utils.replace_cib_configuration(
2629 	        resource_relocate_clear(utils.get_cib_dom())
2630 	    )
2631 	
2632 	
2633 	def resource_relocate_set_stickiness(cib_dom, resources=None):
2634 	    """
2635 	    Commandline options: no options
2636 	    """
2637 	    resources = [] if resources is None else resources
2638 	    cib_dom = cib_dom.cloneNode(True)  # do not change the original cib
2639 	    resources_found = set()
2640 	    updated_resources = set()
2641 	    # set stickiness=0
2642 	    for tagname in ("master", "clone", "group", "primitive"):
2643 	        for res_el in cib_dom.getElementsByTagName(tagname):
2644 	            if resources and res_el.getAttribute("id") not in resources:
2645 	                continue
2646 	            resources_found.add(res_el.getAttribute("id"))
2647 	            res_and_children = (
2648 	                [res_el]
2649 	                + res_el.getElementsByTagName("group")
2650 	                + res_el.getElementsByTagName("primitive")
2651 	            )
2652 	            updated_resources.update(
2653 	                [el.getAttribute("id") for el in res_and_children]
2654 	            )
2655 	            for res_or_child in res_and_children:
2656 	                meta_attributes = utils.dom_prepare_child_element(
2657 	                    res_or_child,
2658 	                    "meta_attributes",
2659 	                    res_or_child.getAttribute("id") + "-meta_attributes",
2660 	                )
2661 	                utils.dom_update_nv_pair(
2662 	                    meta_attributes,
2663 	                    "resource-stickiness",
2664 	                    "0",
2665 	                    meta_attributes.getAttribute("id") + "-",
2666 	                )
2667 	    # resources don't exist
2668 	    if resources:
2669 	        resources_not_found = set(resources) - resources_found
2670 	        if resources_not_found:
2671 	            for res_id in resources_not_found:
2672 	                utils.err(
2673 	                    "unable to find a resource/clone/group: {0}".format(res_id),
2674 	                    False,
2675 	                )
2676 	            sys.exit(1)
2677 	    return cib_dom, updated_resources
2678 	
2679 	
2680 	def resource_relocate_get_locations(cib_dom, resources=None):
2681 	    """
2682 	    Commandline options:
2683 	      * --force - allow constraint on any resource, may not have any effective
2684 	        as an invalid constraint is ignored anyway
2685 	    """
2686 	    resources = [] if resources is None else resources
2687 	    updated_cib, updated_resources = resource_relocate_set_stickiness(
2688 	        cib_dom, resources
2689 	    )
2690 	    dummy_simout, transitions, new_cib = utils.simulate_cib(updated_cib)
2691 	    operation_list = utils.get_operations_from_transitions(transitions)
2692 	    locations = utils.get_resources_location_from_operations(
2693 	        new_cib, operation_list
2694 	    )
2695 	    # filter out non-requested resources
2696 	    if not resources:
2697 	        return list(locations.values())
2698 	    return [
2699 	        val
2700 	        for val in locations.values()
2701 	        if val["id"] in updated_resources
2702 	        or val["id_for_constraint"] in updated_resources
2703 	    ]
2704 	
2705 	
2706 	def resource_relocate_show(cib_dom):
2707 	    """
2708 	    Commandline options: no options
2709 	    """
2710 	    updated_cib, dummy_updated_resources = resource_relocate_set_stickiness(
2711 	        cib_dom
2712 	    )
2713 	    simout, dummy_transitions, dummy_new_cib = utils.simulate_cib(updated_cib)
2714 	    in_status = False
2715 	    in_status_resources = False
2716 	    in_transitions = False
2717 	    for line in simout.split("\n"):
2718 	        if line.strip() == "Current cluster status:":
2719 	            in_status = True
2720 	            in_status_resources = False
2721 	            in_transitions = False
2722 	        elif line.strip() == "Transition Summary:":
2723 	            in_status = False
2724 	            in_status_resources = False
2725 	            in_transitions = True
2726 	            print()
2727 	        elif line.strip() == "":
2728 	            if in_status:
2729 	                in_status = False
2730 	                in_status_resources = True
2731 	                in_transitions = False
2732 	            else:
2733 	                in_status = False
2734 	                in_status_resources = False
2735 	                in_transitions = False
2736 	        if in_status or in_status_resources or in_transitions:
2737 	            print(line)
2738 	
2739 	
2740 	def resource_relocate_location_to_str(location):
2741 	    """
2742 	    Commandline options: no options
2743 	    """
2744 	    message = (
2745 	        "Creating location constraint: {res} prefers {node}=INFINITY{role}"
2746 	    )
2747 	    if "start_on_node" in location:
2748 	        return message.format(
2749 	            res=location["id_for_constraint"],
2750 	            node=location["start_on_node"],
2751 	            role="",
2752 	        )
2753 	    if "promote_on_node" in location:
2754 	        return message.format(
2755 	            res=location["id_for_constraint"],
2756 	            node=location["promote_on_node"],
2757 	            role=f" role={const.PCMK_ROLE_PROMOTED}",
2758 	        )
2759 	    return ""
2760 	
2761 	
2762 	def resource_relocate_run(cib_dom, resources=None, dry=True):  # noqa: PLR0912
2763 	    """
2764 	    Commandline options:
2765 	      * -f - CIB file, explicitly forbids -f if dry is False
2766 	      * --force - allow constraint on any resource, may not have any effective
2767 	        as an invalid copnstraint is ignored anyway
2768 	    """
2769 	    resources = [] if resources is None else resources
2770 	    was_error = False
2771 	    anything_changed = False
2772 	    if not dry and utils.usefile:
2773 	        utils.err("This command cannot be used with -f")
2774 	
2775 	    # create constraints
2776 	    cib_dom, constraint_el = constraint.getCurrentConstraints(cib_dom)
2777 	    for location in resource_relocate_get_locations(cib_dom, resources):
2778 	        if not ("start_on_node" in location or "promote_on_node" in location):
2779 	            continue
2780 	        anything_changed = True
2781 	        print_to_stderr(resource_relocate_location_to_str(location))
2782 	        constraint_id = utils.find_unique_id(
2783 	            cib_dom,
2784 	            RESOURCE_RELOCATE_CONSTRAINT_PREFIX + location["id_for_constraint"],
2785 	        )
2786 	        new_constraint = cib_dom.createElement("rsc_location")
2787 	        new_constraint.setAttribute("id", constraint_id)
2788 	        new_constraint.setAttribute("rsc", location["id_for_constraint"])
2789 	        new_constraint.setAttribute("score", "INFINITY")
2790 	        if "promote_on_node" in location:
2791 	            new_constraint.setAttribute("node", location["promote_on_node"])
2792 	            new_constraint.setAttribute(
2793 	                "role",
2794 	                pacemaker.role.get_value_for_cib(
2795 	                    const.PCMK_ROLE_PROMOTED,
2796 	                    utils.isCibVersionSatisfied(
2797 	                        cib_dom, const.PCMK_NEW_ROLES_CIB_VERSION
2798 	                    ),
2799 	                ),
2800 	            )
2801 	        elif "start_on_node" in location:
2802 	            new_constraint.setAttribute("node", location["start_on_node"])
2803 	        constraint_el.appendChild(new_constraint)
2804 	    if not anything_changed:
2805 	        return
2806 	    if not dry:
2807 	        utils.replace_cib_configuration(cib_dom)
2808 	
2809 	    # wait for resources to move
2810 	    print_to_stderr("\nWaiting for resources to move...\n")
2811 	    if not dry:
2812 	        output, retval = utils.run(["crm_resource", "--wait"])
2813 	        if retval != 0:
2814 	            was_error = True
2815 	            if retval == PACEMAKER_WAIT_TIMEOUT_STATUS:
2816 	                utils.err("waiting timeout", False)
2817 	            else:
2818 	                utils.err(output, False)
2819 	
2820 	    # remove constraints
2821 	    resource_relocate_clear(cib_dom)
2822 	    if not dry:
2823 	        utils.replace_cib_configuration(cib_dom)
2824 	
2825 	    if was_error:
2826 	        sys.exit(1)
2827 	
2828 	
2829 	def resource_relocate_clear(cib_dom):
2830 	    """
2831 	    Commandline options: no options
2832 	    """
2833 	    for constraint_el in cib_dom.getElementsByTagName("constraints"):
2834 	        for location_el in constraint_el.getElementsByTagName("rsc_location"):
2835 	            location_id = location_el.getAttribute("id")
2836 	            if location_id.startswith(RESOURCE_RELOCATE_CONSTRAINT_PREFIX):
2837 	                print_to_stderr("Removing constraint {0}".format(location_id))
2838 	                location_el.parentNode.removeChild(location_el)
2839 	    return cib_dom
2840 	
2841 	
2842 	def set_resource_utilization(resource_id: str, argv: Argv) -> None:
2843 	    """
2844 	    Commandline options:
2845 	      * -f - CIB file
2846 	    """
2847 	    cib = utils.get_cib_dom()
2848 	    resource_el = utils.dom_get_resource(cib, resource_id)
2849 	    if resource_el is None:
2850 	        utils.err("Unable to find a resource: {0}".format(resource_id))
2851 	    utils.dom_update_utilization(resource_el, KeyValueParser(argv).get_unique())
2852 	    utils.replace_cib_configuration(cib)
2853 	
2854 	
2855 	def print_resource_utilization(resource_id: str) -> None:
2856 	    """
2857 	    Commandline options:
2858 	      * -f - CIB file
2859 	    """
2860 	    cib = utils.get_cib_dom()
2861 	    resource_el = utils.dom_get_resource(cib, resource_id)
2862 	    if resource_el is None:
2863 	        utils.err("Unable to find a resource: {0}".format(resource_id))
2864 	    utilization = utils.get_utilization_str(resource_el)
2865 	
2866 	    print("Resource Utilization:")
2867 	    print(" {0}: {1}".format(resource_id, utilization))
2868 	
2869 	
2870 	def print_resources_utilization() -> None:
2871 	    """
2872 	    Commandline options:
2873 	      * -f - CIB file
2874 	    """
2875 	    cib = utils.get_cib_dom()
2876 	    utilization = {}
2877 	    for resource_el in cib.getElementsByTagName("primitive"):
2878 	        utilization_str = utils.get_utilization_str(resource_el)
2879 	        if utilization_str:
2880 	            utilization[resource_el.getAttribute("id")] = utilization_str
2881 	
2882 	    print("Resource Utilization:")
2883 	    for resource in sorted(utilization):
2884 	        print(" {0}: {1}".format(resource, utilization[resource]))
2885 	
2886 	
2887 	def resource_bundle_create_cmd(
2888 	    lib: Any, argv: Argv, modifiers: InputModifiers
2889 	) -> None:
2890 	    """
2891 	    Options:
2892 	      * --force - allow unknown options
2893 	      * --disabled - create as a stopped bundle
2894 	      * --wait
2895 	      * -f - CIB file
2896 	    """
2897 	    modifiers.ensure_only_supported("--force", "--disabled", "--wait", "-f")
2898 	    if not argv:
2899 	        raise CmdLineInputError()
2900 	
2901 	    bundle_id = argv[0]
2902 	    parts = parse_bundle_create_options(argv[1:])
2903 	    lib.resource.bundle_create(
2904 	        bundle_id,
2905 	        parts.container_type,
2906 	        container_options=parts.container,
2907 	        network_options=parts.network,
2908 	        port_map=parts.port_map,
2909 	        storage_map=parts.storage_map,
2910 	        meta_attributes=parts.meta_attrs,
2911 	        force_options=modifiers.get("--force"),
2912 	        ensure_disabled=modifiers.get("--disabled"),
2913 	        wait=modifiers.get("--wait"),
2914 	    )
2915 	
2916 	
2917 	def resource_bundle_reset_cmd(
2918 	    lib: Any, argv: Argv, modifiers: InputModifiers
2919 	) -> None:
2920 	    """
2921 	    Options:
2922 	      * --force - allow unknown options
2923 	      * --disabled - create as a stopped bundle
2924 	      * --wait
2925 	      * -f - CIB file
2926 	    """
2927 	    modifiers.ensure_only_supported("--force", "--disabled", "--wait", "-f")
2928 	    if not argv:
2929 	        raise CmdLineInputError()
2930 	
2931 	    bundle_id = argv[0]
2932 	    parts = parse_bundle_reset_options(argv[1:])
2933 	    lib.resource.bundle_reset(
2934 	        bundle_id,
2935 	        container_options=parts.container,
2936 	        network_options=parts.network,
2937 	        port_map=parts.port_map,
2938 	        storage_map=parts.storage_map,
2939 	        meta_attributes=parts.meta_attrs,
2940 	        force_options=modifiers.get("--force"),
2941 	        ensure_disabled=modifiers.get("--disabled"),
2942 	        wait=modifiers.get("--wait"),
2943 	    )
2944 	
2945 	
2946 	def resource_bundle_update_cmd(
2947 	    lib: Any, argv: Argv, modifiers: InputModifiers
2948 	) -> None:
2949 	    """
2950 	    Options:
2951 	      * --force - allow unknown options
2952 	      * --wait
2953 	      * -f - CIB file
2954 	    """
2955 	    modifiers.ensure_only_supported("--force", "--wait", "-f")
2956 	    if not argv:
2957 	        raise CmdLineInputError()
2958 	
2959 	    bundle_id = argv[0]
2960 	    parts = parse_bundle_update_options(argv[1:])
2961 	    lib.resource.bundle_update(
2962 	        bundle_id,
2963 	        container_options=parts.container,
2964 	        network_options=parts.network,
2965 	        port_map_add=parts.port_map_add,
2966 	        port_map_remove=parts.port_map_remove,
2967 	        storage_map_add=parts.storage_map_add,
2968 	        storage_map_remove=parts.storage_map_remove,
2969 	        meta_attributes=parts.meta_attrs,
2970 	        force_options=modifiers.get("--force"),
2971 	        wait=modifiers.get("--wait"),
2972 	    )
2973