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