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