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