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