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