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