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