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