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