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