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