1 import sys
2 import xml.dom.minidom
3 from collections.abc import Iterable
4 from enum import Enum
5 from typing import Any, TypeVar, cast
6 from xml.dom.minidom import parseString
7
8 import pcs.cli.constraint_order.command as order_command
9 from pcs import utils
10 from pcs.cli.common import parse_args
11 from pcs.cli.common.errors import (
12 SEE_MAN_CHANGES,
13 CmdLineInputError,
14 raise_command_replaced,
15 )
16 from pcs.cli.common.output import INDENT_STEP, lines_to_str
17 from pcs.cli.constraint.location.command import (
18 RESOURCE_TYPE_REGEXP,
19 RESOURCE_TYPE_RESOURCE,
20 )
21 from pcs.cli.constraint.output import (
22 CibConstraintLocationAnyDto,
23 filter_constraints_by_rule_expired_status,
24 location,
25 print_config,
26 )
27 from pcs.cli.reports import process_library_reports
28 from pcs.cli.reports.output import deprecation_warning, print_to_stderr, warn
29 from pcs.common import const, pacemaker, reports
30 from pcs.common.pacemaker.constraint import (
31 CibConstraintColocationSetDto,
32 CibConstraintLocationSetDto,
33 CibConstraintOrderSetDto,
34 CibConstraintsDto,
35 CibConstraintTicketSetDto,
36 get_all_constraints_ids,
37 )
38 from pcs.common.pacemaker.resource.list import CibResourcesDto
39 from pcs.common.pacemaker.types import CibResourceDiscovery
40 from pcs.common.reports import ReportItem
41 from pcs.common.str_tools import format_list, indent
42 from pcs.common.types import StringCollection, StringIterable, StringSequence
43 from pcs.lib.cib.constraint.order import ATTRIB as order_attrib
44 from pcs.lib.node import get_existing_nodes_names
45 from pcs.lib.pacemaker.values import (
46 SCORE_INFINITY,
47 is_score,
48 is_true,
49 sanitize_id,
50 )
51
52 DEFAULT_ACTION = const.PCMK_ACTION_START
53 DEFAULT_ROLE = const.PCMK_ROLE_STARTED
54
55 OPTIONS_SYMMETRICAL = order_attrib["symmetrical"]
56
57 LOCATION_NODE_VALIDATION_SKIP_MSG = (
58 "Validation for node existence in the cluster will be skipped"
59 )
60
61
62 class CrmRuleReturnCode(Enum):
63 IN_EFFECT = 0
64 EXPIRED = 110
65 TO_BE_IN_EFFECT = 111
66
67
68 def _hint_syntax_has_changed(version: str) -> str:
69 return "Hint: Syntax has changed from previous version. {}".format(
70 SEE_MAN_CHANGES.format(version)
71 )
72
73
74 def constraint_order_cmd(lib, argv, modifiers):
75 sub_cmd = "config" if not argv else argv.pop(0)
76
77 try:
78 if sub_cmd == "set":
79 order_command.create_with_set(lib, argv, modifiers)
80 elif sub_cmd in ["remove", "delete"]:
81 order_rm(lib, argv, modifiers)
82 elif sub_cmd == "show":
83 raise_command_replaced(
84 ["pcs constraint order config"], pcs_version="0.12"
85 )
86 elif sub_cmd == "config":
87 order_command.config_cmd(lib, argv, modifiers)
88 else:
89 order_start(lib, [sub_cmd] + argv, modifiers)
90 except CmdLineInputError as e:
91 utils.exit_on_cmdline_input_error(e, "constraint", ["order", sub_cmd])
92
93
94 def config_cmd(
95 lib: Any, argv: list[str], modifiers: parse_args.InputModifiers
96 ) -> None:
97 modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all")
98 if argv:
99 raise CmdLineInputError()
100
101 print_config(
102 cast(
103 CibConstraintsDto,
104 lib.constraint.get_config(evaluate_rules=True),
105 ),
106 modifiers,
107 )
108
109
110 def _validate_constraint_resource(cib_dom, resource_id):
111 (
112 resource_valid,
113 resource_error,
114 dummy_correct_id,
115 ) = utils.validate_constraint_resource(cib_dom, resource_id)
116 if not resource_valid:
117 utils.err(resource_error)
118
119
120 def _validate_resources_not_in_same_group(cib_dom, resource1, resource2):
121 if not utils.validate_resources_not_in_same_group(
122 cib_dom, resource1, resource2
123 ):
124 utils.err(
125 "Cannot create an order constraint for resources in the same group"
126 )
127
128
129 # Syntax: colocation add [role] <src> with [role] <tgt> [score=<score>] [options]
130 # possible commands:
131 # <src> with <tgt> [score=<score>] [options]
132 # <src> with <role> <tgt> [score=<score>] [options]
133 # <role> <src> with <tgt> [score=<score>] [options]
134 # <role> <src> with <role> <tgt> [score=<score>] [options]
135 def colocation_add(lib, argv, modifiers): # noqa: PLR0912, PLR0915
136 """
137 Options:
138 * -f - CIB file
139 * --force - allow constraint on any resource, allow duplicate constraints
140 """
141
142 def _validate_and_prepare_role(new_roles_supported, role):
143 if role is None:
144 return ""
145 role_cleaned = role.lower().capitalize()
146 if role_cleaned not in const.PCMK_ROLES:
147 utils.err(
148 (
149 "invalid role value '{0}', allowed values are: {1}\n{2}"
150 ).format(
151 role,
152 format_list(const.PCMK_ROLES),
153 _hint_syntax_has_changed("1.0"),
154 )
155 )
156 return pacemaker.role.get_value_for_cib(
157 role_cleaned, new_roles_supported
158 )
159
160 del lib
161 modifiers.ensure_only_supported("-f", "--force")
162 if len(argv) < 3:
163 raise CmdLineInputError()
164
165 role1_candidate = None
166 role2_candidate = None
167
168 if argv[2] == "with":
169 role1_candidate = argv.pop(0)
170 resource1 = argv.pop(0)
171 elif argv[1] == "with":
172 resource1 = argv.pop(0)
173 else:
174 raise CmdLineInputError()
175
176 if argv.pop(0) != "with":
177 raise CmdLineInputError()
178 if "with" in argv:
179 raise CmdLineInputError(
180 message="Multiple 'with's cannot be specified.",
181 hint=(
182 "Use the 'pcs constraint colocation set' command if you want "
183 "to create a constraint for more than two resources."
184 ),
185 show_both_usage_and_message=True,
186 )
187
188 if not argv:
189 raise CmdLineInputError()
190 if len(argv) == 1 or "=" in argv[1]:
191 resource2 = argv.pop(0)
192 else:
193 role2_candidate = argv.pop(0)
194 resource2 = argv.pop(0)
195
196 nv_pairs = [
197 parse_args.split_option(arg, allow_empty_value=False) for arg in argv
198 ]
199 influence_attr_set = any(name == "influence" for name, _ in nv_pairs)
200
201 cib_dom = (
202 utils.cluster_upgrade_to_version(
203 const.PCMK_COLOCATION_INFLUENCE_CIB_VERSION
204 )
205 if influence_attr_set
206 else utils.get_cib_dom()
207 )
208 new_roles_supported = utils.isCibVersionSatisfied(
209 cib_dom, const.PCMK_NEW_ROLES_CIB_VERSION
210 )
211
212 role1 = _validate_and_prepare_role(new_roles_supported, role1_candidate)
213 role2 = _validate_and_prepare_role(new_roles_supported, role2_candidate)
214 _validate_constraint_resource(cib_dom, resource1)
215 _validate_constraint_resource(cib_dom, resource2)
216
217 id_in_nvpairs = None
218 score = None
219 for name, value in nv_pairs:
220 if name == "id":
221 id_valid, id_error = utils.validate_xml_id(value, "constraint id")
222 if not id_valid:
223 utils.err(id_error)
224 if utils.does_id_exist(cib_dom, value):
225 utils.err(
226 "id '%s' is already in use, please specify another one"
227 % value
228 )
229 id_in_nvpairs = True
230 elif name == "score":
231 score = value
232 if score is None:
233 score = SCORE_INFINITY
234 if not id_in_nvpairs:
235 nv_pairs.append(
236 (
237 "id",
238 utils.find_unique_id(
239 cib_dom,
240 "colocation-%s-%s-%s" % (resource1, resource2, score),
241 ),
242 )
243 )
244
245 (dom, constraintsElement) = getCurrentConstraints(cib_dom)
246
247 # If one role is specified, the other should default to "started"
248 if role1 != "" and role2 == "":
249 role2 = DEFAULT_ROLE
250 if role2 != "" and role1 == "":
251 role1 = DEFAULT_ROLE
252 element = dom.createElement("rsc_colocation")
253 element.setAttribute("rsc", resource1)
254 element.setAttribute("with-rsc", resource2)
255 element.setAttribute("score", score)
256 if role1 != "":
257 element.setAttribute("rsc-role", role1)
258 if role2 != "":
259 element.setAttribute("with-rsc-role", role2)
260 for nv_pair in nv_pairs:
261 element.setAttribute(nv_pair[0], nv_pair[1])
262 if not modifiers.get("--force"):
263
264 def _constraint_export(constraint_info):
265 options_dict = constraint_info["options"]
266 co_resource1 = options_dict.get("rsc", "")
267 co_resource2 = options_dict.get("with-rsc", "")
268 co_id = options_dict.get("id", "")
269 co_score = options_dict.get("score", "")
270 score_text = "(score:" + co_score + ")"
271 console_option_list = [
272 f"({option[0]}:{option[1]})"
273 for option in sorted(options_dict.items())
274 if option[0] not in ("rsc", "with-rsc", "id", "score")
275 ]
276 console_option_list.append(f"(id:{co_id})")
277 return " ".join(
278 [co_resource1, "with", co_resource2, score_text]
279 + console_option_list
280 )
281
282 duplicates = colocation_find_duplicates(constraintsElement, element)
283 if duplicates:
284 utils.err(
285 "duplicate constraint already exists, use --force to override\n"
286 + "\n".join(
287 [
288 " "
289 + _constraint_export(
290 {"options": dict(dup.attributes.items())}
291 )
292 for dup in duplicates
293 ]
294 )
295 )
296 constraintsElement.appendChild(element)
297 utils.replace_cib_configuration(dom)
298
299
300 def colocation_find_duplicates(dom, constraint_el):
301 """
302 Commandline options: no options
303 """
304 new_roles_supported = utils.isCibVersionSatisfied(
305 dom, const.PCMK_NEW_ROLES_CIB_VERSION
306 )
307
308 def normalize(const_el):
309 return (
310 const_el.getAttribute("rsc"),
311 const_el.getAttribute("with-rsc"),
312 pacemaker.role.get_value_for_cib(
313 const_el.getAttribute("rsc-role").capitalize() or DEFAULT_ROLE,
314 new_roles_supported,
315 ),
316 pacemaker.role.get_value_for_cib(
317 const_el.getAttribute("with-rsc-role").capitalize()
318 or DEFAULT_ROLE,
319 new_roles_supported,
320 ),
321 )
322
323 normalized_el = normalize(constraint_el)
324 return [
325 other_el
326 for other_el in dom.getElementsByTagName("rsc_colocation")
327 if not other_el.getElementsByTagName("resource_set")
328 and constraint_el is not other_el
329 and normalized_el == normalize(other_el)
330 ]
331
332
333 def order_rm(lib, argv, modifiers):
334 """
335 Options:
336 * -f - CIB file
337 """
338 del lib
339 modifiers.ensure_only_supported("-f")
340 if not argv:
341 raise CmdLineInputError()
342
343 elementFound = False
344 (dom, constraintsElement) = getCurrentConstraints()
345
346 for resource in argv:
347 for ord_loc in constraintsElement.getElementsByTagName("rsc_order")[:]:
348 if (
349 ord_loc.getAttribute("first") == resource
350 or ord_loc.getAttribute("then") == resource
351 ):
352 constraintsElement.removeChild(ord_loc)
353 elementFound = True
354
355 resource_refs_to_remove = []
356 for ord_set in constraintsElement.getElementsByTagName("resource_ref"):
357 if ord_set.getAttribute("id") == resource:
358 resource_refs_to_remove.append(ord_set)
359 elementFound = True
360
361 for res_ref in resource_refs_to_remove:
362 res_set = res_ref.parentNode
363 res_order = res_set.parentNode
364
365 res_ref.parentNode.removeChild(res_ref)
366 if not res_set.getElementsByTagName("resource_ref"):
367 res_set.parentNode.removeChild(res_set)
368 if not res_order.getElementsByTagName("resource_set"):
369 res_order.parentNode.removeChild(res_order)
370
371 if elementFound:
372 utils.replace_cib_configuration(dom)
373 else:
374 utils.err("No matching resources found in ordering list")
375
376
377 def order_start(lib, argv, modifiers):
378 """
379 Options:
380 * -f - CIB file
381 * --force - allow constraint for any resource, allow duplicate constraints
382 """
383 del lib
384 modifiers.ensure_only_supported("-f", "--force")
385 if len(argv) < 3:
386 raise CmdLineInputError()
387
388 first_action = DEFAULT_ACTION
389 then_action = DEFAULT_ACTION
390 action = argv[0]
391 if action in const.PCMK_ACTIONS:
392 first_action = action
393 argv.pop(0)
394
395 resource1 = argv.pop(0)
396 if argv.pop(0) != "then":
397 raise CmdLineInputError()
398
399 if not argv:
400 raise CmdLineInputError()
401
402 action = argv[0]
403 if action in const.PCMK_ACTIONS:
404 then_action = action
405 argv.pop(0)
406
407 if not argv:
408 raise CmdLineInputError()
409 resource2 = argv.pop(0)
410
411 order_options = []
412 if argv:
413 order_options = order_options + argv[:]
414 if "then" in order_options:
415 raise CmdLineInputError(
416 message="Multiple 'then's cannot be specified.",
417 hint=(
418 "Use the 'pcs constraint order set' command if you want to "
419 "create a constraint for more than two resources."
420 ),
421 show_both_usage_and_message=True,
422 )
423
424 order_options.append("first-action=" + first_action)
425 order_options.append("then-action=" + then_action)
426 _order_add(resource1, resource2, order_options, modifiers)
427
428
429 def _order_add(resource1, resource2, options_list, modifiers): # noqa: PLR0912, PLR0915
430 """
431 Commandline options:
432 * -f - CIB file
433 * --force - allow constraint for any resource, allow duplicate constraints
434 """
435 cib_dom = utils.get_cib_dom()
436 _validate_constraint_resource(cib_dom, resource1)
437 _validate_constraint_resource(cib_dom, resource2)
438
439 _validate_resources_not_in_same_group(cib_dom, resource1, resource2)
440
441 order_options = []
442 id_specified = False
443 sym = None
444 for arg in options_list:
445 if arg == "symmetrical":
446 sym = "true"
447 elif arg == "nonsymmetrical":
448 sym = "false"
449 else:
450 name, value = parse_args.split_option(arg, allow_empty_value=False)
451 if name == "id":
452 id_valid, id_error = utils.validate_xml_id(
453 value, "constraint id"
454 )
455 if not id_valid:
456 utils.err(id_error)
457 if utils.does_id_exist(cib_dom, value):
458 utils.err(
459 "id '%s' is already in use, please specify another one"
460 % value
461 )
462 id_specified = True
463 order_options.append((name, value))
464 elif name == "symmetrical":
465 if value.lower() in OPTIONS_SYMMETRICAL:
466 sym = value.lower()
467 else:
468 utils.err(
469 "invalid symmetrical value '%s', allowed values are: %s"
470 % (value, ", ".join(OPTIONS_SYMMETRICAL))
471 )
472 else:
473 order_options.append((name, value))
474 if sym:
475 order_options.append(("symmetrical", sym))
476
477 options = ""
478 if order_options:
479 options = " (Options: %s)" % " ".join(
480 [
481 "%s=%s" % (name, value)
482 for name, value in order_options
483 if name not in ("kind", "score")
484 ]
485 )
486
487 scorekind = "kind: Mandatory"
488 id_suffix = "mandatory"
489 for opt in order_options:
490 if opt[0] == "score":
491 scorekind = "score: " + opt[1]
492 id_suffix = opt[1]
493 # TODO deprecated in pacemaker 2, to be removed in pacemaker 3
494 # added to pcs after 0.11.7
495 deprecation_warning(
496 reports.messages.DeprecatedOption(opt[0], []).message
497 )
498 break
499 if opt[0] == "kind":
500 scorekind = "kind: " + opt[1]
501 id_suffix = opt[1]
502 break
503
504 if not id_specified:
505 order_id = "order-" + resource1 + "-" + resource2 + "-" + id_suffix
506 order_id = utils.find_unique_id(cib_dom, order_id)
507 order_options.append(("id", order_id))
508
509 (dom, constraintsElement) = getCurrentConstraints()
510 element = dom.createElement("rsc_order")
511 element.setAttribute("first", resource1)
512 element.setAttribute("then", resource2)
513 for order_opt in order_options:
514 element.setAttribute(order_opt[0], order_opt[1])
515 constraintsElement.appendChild(element)
516 if not modifiers.get("--force"):
517
518 def _constraint_export(constraint_info):
519 options = constraint_info["options"]
520 oc_resource1 = options.get("first", "")
521 oc_resource2 = options.get("then", "")
522 first_action = options.get("first-action", "")
523 then_action = options.get("then-action", "")
524 oc_id = options.get("id", "")
525 oc_score = options.get("score", "")
526 oc_kind = options.get("kind", "")
527 oc_sym = ""
528 oc_id_out = ""
529 oc_options = ""
530 if "symmetrical" in options and not is_true(
531 options.get("symmetrical", "false")
532 ):
533 oc_sym = "(non-symmetrical)"
534 if oc_kind != "":
535 score_text = "(kind:" + oc_kind + ")"
536 elif oc_kind == "" and oc_score == "":
537 score_text = "(kind:Mandatory)"
538 else:
539 score_text = "(score:" + oc_score + ")"
540 oc_id_out = "(id:" + oc_id + ")"
541 already_processed_options = (
542 "first",
543 "then",
544 "first-action",
545 "then-action",
546 "id",
547 "score",
548 "kind",
549 "symmetrical",
550 )
551 oc_options = " ".join(
552 [
553 f"{name}={value}"
554 for name, value in options.items()
555 if name not in already_processed_options
556 ]
557 )
558 if oc_options:
559 oc_options = "(Options: " + oc_options + ")"
560 return " ".join(
561 [
562 arg
563 for arg in [
564 first_action,
565 oc_resource1,
566 "then",
567 then_action,
568 oc_resource2,
569 score_text,
570 oc_sym,
571 oc_options,
572 oc_id_out,
573 ]
574 if arg
575 ]
576 )
577
578 duplicates = order_find_duplicates(constraintsElement, element)
579 if duplicates:
580 utils.err(
581 "duplicate constraint already exists, use --force to override\n"
582 + "\n".join(
583 [
584 " "
585 + _constraint_export(
586 {"options": dict(dup.attributes.items())}
587 )
588 for dup in duplicates
589 ]
590 )
591 )
592 print_to_stderr(f"Adding {resource1} {resource2} ({scorekind}){options}")
593 utils.replace_cib_configuration(dom)
594
595
596 def order_find_duplicates(dom, constraint_el):
597 """
598 Commandline options: no options
599 """
600
601 def normalize(constraint_el):
602 return (
603 constraint_el.getAttribute("first"),
604 constraint_el.getAttribute("then"),
605 constraint_el.getAttribute("first-action").lower()
606 or DEFAULT_ACTION,
607 constraint_el.getAttribute("then-action").lower() or DEFAULT_ACTION,
608 )
609
610 normalized_el = normalize(constraint_el)
611 return [
612 other_el
613 for other_el in dom.getElementsByTagName("rsc_order")
614 if not other_el.getElementsByTagName("resource_set")
615 and constraint_el is not other_el
616 and normalized_el == normalize(other_el)
617 ]
618
619
620 _SetConstraint = TypeVar(
621 "_SetConstraint",
622 CibConstraintLocationSetDto,
623 CibConstraintColocationSetDto,
624 CibConstraintOrderSetDto,
625 CibConstraintTicketSetDto,
626 )
627
628
629 def _filter_set_constraints_by_resources(
630 constraints_dto: Iterable[_SetConstraint], resources: set[str]
631 ) -> list[_SetConstraint]:
632 return [
633 constraint_set_dto
634 for constraint_set_dto in constraints_dto
635 if any(
636 set(resource_set.resources_ids) & resources
637 for resource_set in constraint_set_dto.resource_sets
638 )
639 ]
640
641
642 def _filter_constraints_by_resources(
643 constraints_dto: CibConstraintsDto,
644 resources: StringIterable,
645 patterns: StringIterable,
646 ) -> CibConstraintsDto:
647 required_resources_set = set(resources)
648 required_patterns_set = set(patterns)
649 return CibConstraintsDto(
650 location=[
651 constraint_dto
652 for constraint_dto in constraints_dto.location
653 if (
654 constraint_dto.resource_id is not None
655 and constraint_dto.resource_id in required_resources_set
656 )
657 or (
658 constraint_dto.resource_pattern is not None
659 and constraint_dto.resource_pattern in required_patterns_set
660 )
661 ],
662 location_set=_filter_set_constraints_by_resources(
663 constraints_dto.location_set, required_resources_set
664 ),
665 colocation=[
666 constraint_dto
667 for constraint_dto in constraints_dto.colocation
668 if {constraint_dto.resource_id, constraint_dto.with_resource_id}
669 & required_resources_set
670 ],
671 colocation_set=_filter_set_constraints_by_resources(
672 constraints_dto.colocation_set, required_resources_set
673 ),
674 order=[
675 constraint_dto
676 for constraint_dto in constraints_dto.order
677 if {
678 constraint_dto.first_resource_id,
679 constraint_dto.then_resource_id,
680 }
681 & required_resources_set
682 ],
683 order_set=_filter_set_constraints_by_resources(
684 constraints_dto.order_set, required_resources_set
685 ),
686 ticket=[
687 constraint_dto
688 for constraint_dto in constraints_dto.ticket
689 if constraint_dto.resource_id in required_resources_set
690 ],
691 ticket_set=_filter_set_constraints_by_resources(
692 constraints_dto.ticket_set, required_resources_set
693 ),
694 )
695
696
697 def _filter_location_by_node_base(
698 constraint_dtos: Iterable[CibConstraintLocationAnyDto],
699 nodes: StringCollection,
700 ) -> list[CibConstraintLocationAnyDto]:
701 return [
702 constraint_dto
703 for constraint_dto in constraint_dtos
704 if constraint_dto.attributes.node is not None
705 and constraint_dto.attributes.node in nodes
706 ]
707
708
709 def location_config_cmd(
710 lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers
711 ) -> None:
712 """
713 Options:
714 * --all - print expired constraints
715 * --full - print all details
716 * -f - CIB file
717 """
718 modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all")
719 filter_type: str | None = None
720 filter_items: parse_args.Argv = []
721 if argv:
722 filter_type, *filter_items = argv
723 allowed_types = ("resources", "nodes")
724 if filter_type not in allowed_types:
725 raise CmdLineInputError(
726 f"Unknown keyword '{filter_type}'. Allowed keywords: "
727 f"{format_list(allowed_types)}"
728 )
729 if modifiers.get_output_format() != parse_args.OUTPUT_FORMAT_VALUE_TEXT:
730 raise CmdLineInputError(
731 "Output formats other than 'text' are not supported together "
732 "with grouping and filtering by nodes or resources"
733 )
734
735 constraints_dto = filter_constraints_by_rule_expired_status(
736 lib.constraint.get_config(evaluate_rules=True),
737 modifiers.is_specified("--all"),
738 )
739
740 constraints_dto = CibConstraintsDto(
741 location=constraints_dto.location,
742 location_set=constraints_dto.location_set,
743 )
744
745 def _print_lines(lines: StringSequence) -> None:
746 if lines:
747 print("Location Constraints:")
748 print(lines_to_str(indent(lines, indent_step=INDENT_STEP)))
749
750 if filter_type == "resources":
751 if filter_items:
752 resources = []
753 patterns = []
754 for item in filter_items:
755 item_type, item_value = parse_args.parse_typed_arg(
756 item,
757 [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP],
758 RESOURCE_TYPE_RESOURCE,
759 )
760 if item_type == RESOURCE_TYPE_RESOURCE:
761 resources.append(item_value)
762 elif item_type == RESOURCE_TYPE_REGEXP:
763 patterns.append(item_value)
764 constraints_dto = _filter_constraints_by_resources(
765 constraints_dto, resources, patterns
766 )
767 _print_lines(
768 location.constraints_to_grouped_by_resource_text(
769 constraints_dto.location,
770 modifiers.is_specified("--full"),
771 )
772 )
773 return
774 if filter_type == "nodes":
775 if filter_items:
776 constraints_dto = CibConstraintsDto(
777 location=_filter_location_by_node_base(
778 constraints_dto.location, filter_items
779 ),
780 location_set=_filter_location_by_node_base(
781 constraints_dto.location_set, filter_items
782 ),
783 )
784 _print_lines(
785 location.constraints_to_grouped_by_node_text(
786 constraints_dto.location,
787 modifiers.is_specified("--full"),
788 )
789 )
790 return
791
792 print_config(constraints_dto, modifiers)
793
794
795 def _verify_node_name(node, existing_nodes):
796 report_list = []
797 if node not in existing_nodes:
798 report_list.append(
799 ReportItem.error(
800 reports.messages.NodeNotFound(node),
801 force_code=reports.codes.FORCE,
802 )
803 )
804 return report_list
805
806
807 def _verify_score(score):
808 if not is_score(score):
809 utils.err(
810 "invalid score '%s', use integer or INFINITY or -INFINITY" % score
811 )
812
813
814 def location_prefer( # noqa: PLR0912
815 lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers
816 ) -> None:
817 """
818 Options:
819 * --force - allow unknown options, allow constraint for any resource type
820 * -f - CIB file
821 """
822 modifiers.ensure_only_supported("--force", "-f")
823 rsc = argv.pop(0)
824 prefer_option = argv.pop(0)
825
826 dummy_rsc_type, rsc_value = parse_args.parse_typed_arg(
827 rsc,
828 [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP],
829 RESOURCE_TYPE_RESOURCE,
830 )
831
832 if prefer_option == "prefers":
833 prefer = True
834 elif prefer_option == "avoids":
835 prefer = False
836 else:
837 raise CmdLineInputError()
838
839 skip_node_check = False
840 existing_nodes: list[str] = []
841 if modifiers.is_specified("-f") or modifiers.get("--force"):
842 skip_node_check = True
843 warn(LOCATION_NODE_VALIDATION_SKIP_MSG)
844 else:
845 lib_env = utils.get_lib_env()
846 existing_nodes, report_list = get_existing_nodes_names(
847 corosync_conf=lib_env.get_corosync_conf(),
848 cib=lib_env.get_cib(),
849 )
850 if report_list:
851 process_library_reports(report_list)
852
853 report_list = []
854 parameters_list = []
855 for nodeconf in argv:
856 nodeconf_a = nodeconf.split("=", 1)
857 node = nodeconf_a[0]
858 if not skip_node_check:
859 report_list += _verify_node_name(node, existing_nodes)
860 if len(nodeconf_a) == 1:
861 score = "INFINITY" if prefer else "-INFINITY"
862 else:
863 score = nodeconf_a[1]
864 _verify_score(score)
865 if not prefer:
866 score = score[1:] if score[0] == "-" else "-" + score
867
868 parameters_list.append(
869 [
870 sanitize_id(f"location-{rsc_value}-{node}-{score}"),
871 rsc,
872 node,
873 f"score={score}",
874 ]
875 )
876
877 if report_list:
878 process_library_reports(report_list)
879
880 modifiers = modifiers.get_subset("--force", "-f")
881
882 for parameters in parameters_list:
883 location_add(lib, parameters, modifiers, skip_score_and_node_check=True)
884
885
886 def location_add( # noqa: PLR0912, PLR0915
887 lib: Any,
888 argv: parse_args.Argv,
889 modifiers: parse_args.InputModifiers,
890 skip_score_and_node_check: bool = False,
891 ) -> None:
892 """
893 Options:
894 * --force - allow unknown options, allow constraint for any resource type
895 * -f - CIB file
896 """
897 del lib
898 modifiers.ensure_only_supported("--force", "-f")
899 if len(argv) < 3:
900 raise CmdLineInputError()
901
902 constraint_id = argv.pop(0)
903 rsc_type, rsc_value = parse_args.parse_typed_arg(
904 argv.pop(0),
905 [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP],
906 RESOURCE_TYPE_RESOURCE,
907 )
908 node = argv.pop(0)
909 score = None
910 if "=" not in argv[0]:
911 utils.err(
912 "Specifying score as a standalone value was removed, use "
913 f"score=value instead\n{_hint_syntax_has_changed('1.0')}"
914 )
915 options = []
916 # For now we only allow setting resource-discovery and score
917 for arg in argv:
918 name, value = parse_args.split_option(arg, allow_empty_value=False)
919 if name == "score":
920 score = value
921 elif name == "resource-discovery":
922 if not modifiers.get("--force"):
923 allowed_discovery = list(
924 map(
925 str,
926 [
927 CibResourceDiscovery.ALWAYS,
928 CibResourceDiscovery.EXCLUSIVE,
929 CibResourceDiscovery.NEVER,
930 ],
931 )
932 )
933 if value not in allowed_discovery:
934 utils.err(
935 (
936 "invalid {0} value '{1}', allowed values are: {2}"
937 ", use --force to override"
938 ).format(name, value, format_list(allowed_discovery))
939 )
940 options.append([name, value])
941 elif modifiers.get("--force"):
942 options.append([name, value])
943 else:
944 utils.err("bad option '%s', use --force to override" % name)
945 if score is None:
946 score = "INFINITY"
947
948 # Verify that specified node exists in the cluster and score is valid
949 if not skip_score_and_node_check:
950 if modifiers.is_specified("-f") or modifiers.get("--force"):
951 warn(LOCATION_NODE_VALIDATION_SKIP_MSG)
952 else:
953 lib_env = utils.get_lib_env()
954 existing_nodes, report_list = get_existing_nodes_names(
955 corosync_conf=lib_env.get_corosync_conf(),
956 cib=lib_env.get_cib(),
957 )
958 report_list += _verify_node_name(node, existing_nodes)
959 if report_list:
960 process_library_reports(report_list)
961 _verify_score(score)
962
963 id_valid, id_error = utils.validate_xml_id(constraint_id, "constraint id")
964 if not id_valid:
965 utils.err(id_error)
966
967 dom = utils.get_cib_dom()
968
969 if rsc_type == RESOURCE_TYPE_RESOURCE:
970 (
971 rsc_valid,
972 rsc_error,
973 dummy_correct_id,
974 ) = utils.validate_constraint_resource(dom, rsc_value)
975 if not rsc_valid:
976 utils.err(rsc_error)
977
978 # Verify current constraint doesn't already exist
979 # If it does we replace it with the new constraint
980 dummy_dom, constraintsElement = getCurrentConstraints(dom)
981 # If the id matches, or the rsc & node match, then we replace/remove
982 elementsToRemove = [
983 rsc_loc
984 for rsc_loc in constraintsElement.getElementsByTagName("rsc_location")
985 if rsc_loc.getAttribute("id") == constraint_id
986 or (
987 rsc_loc.getAttribute("node") == node
988 and (
989 (
990 rsc_type == RESOURCE_TYPE_RESOURCE
991 and rsc_loc.getAttribute("rsc") == rsc_value
992 )
993 or (
994 rsc_type == RESOURCE_TYPE_REGEXP
995 and rsc_loc.getAttribute("rsc-pattern") == rsc_value
996 )
997 )
998 )
999 ]
1000 for etr in elementsToRemove:
1001 constraintsElement.removeChild(etr)
1002
1003 element = dom.createElement("rsc_location")
1004 element.setAttribute("id", constraint_id)
1005 if rsc_type == RESOURCE_TYPE_RESOURCE:
1006 element.setAttribute("rsc", rsc_value)
1007 elif rsc_type == RESOURCE_TYPE_REGEXP:
1008 element.setAttribute("rsc-pattern", rsc_value)
1009 element.setAttribute("node", node)
1010 element.setAttribute("score", score)
1011 for option in options:
1012 element.setAttribute(option[0], option[1])
1013 constraintsElement.appendChild(element)
1014
1015 utils.replace_cib_configuration(dom)
1016
1017
1018 # Grabs the current constraints and returns the dom and constraint element
1019 def getCurrentConstraints(passed_dom=None):
1020 """
1021 Commandline options:
1022 * -f - CIB file, only if passed_dom is None
1023 """
1024 if passed_dom:
1025 dom = passed_dom
1026 else:
1027 current_constraints_xml = utils.get_cib_xpath("//constraints")
1028 if current_constraints_xml == "":
1029 utils.err("unable to process cib")
1030 # Verify current constraint doesn't already exist
1031 # If it does we replace it with the new constraint
|
CID (unavailable; MK=42e4d47d6c846e21a3e7a16e47445de3) (#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. |
1032 dom = parseString(current_constraints_xml)
1033
1034 constraintsElement = dom.getElementsByTagName("constraints")[0]
1035 return (dom, constraintsElement)
1036
1037
1038 # If returnStatus is set, then we don't error out, we just print the error
1039 # and return false
1040 def constraint_rm( # noqa: PLR0912
1041 lib,
1042 argv,
1043 modifiers,
1044 returnStatus=False,
1045 constraintsElement=None,
1046 passed_dom=None,
1047 ):
1048 """
1049 Options:
1050 * -f - CIB file, effective only if passed_dom is None
1051 """
1052 if passed_dom is None:
1053 modifiers.ensure_only_supported("-f")
1054 if not argv:
1055 raise CmdLineInputError()
1056
1057 bad_constraint = False
1058 if len(argv) != 1:
1059 for arg in argv:
1060 if not constraint_rm(
1061 lib, [arg], modifiers, returnStatus=True, passed_dom=passed_dom
1062 ):
1063 bad_constraint = True
1064 if bad_constraint:
1065 sys.exit(1)
1066 return None
1067
1068 c_id = argv.pop(0)
1069 elementFound = False
1070 dom = None
1071 use_cibadmin = False
1072 if not constraintsElement:
1073 (dom, constraintsElement) = getCurrentConstraints(passed_dom)
1074 use_cibadmin = True
1075
1076 for co in constraintsElement.childNodes[:]:
1077 if co.nodeType != xml.dom.Node.ELEMENT_NODE:
1078 continue
1079 if co.getAttribute("id") == c_id:
1080 constraintsElement.removeChild(co)
1081 elementFound = True
1082
1083 if not elementFound:
1084 for rule in constraintsElement.getElementsByTagName("rule")[:]:
1085 if rule.getAttribute("id") == c_id:
1086 elementFound = True
1087 parent = rule.parentNode
1088 parent.removeChild(rule)
1089 if not parent.getElementsByTagName("rule"):
1090 parent.parentNode.removeChild(parent)
1091
1092 if elementFound:
1093 if passed_dom:
1094 return dom
1095 if use_cibadmin:
1096 utils.replace_cib_configuration(dom)
1097 if returnStatus:
1098 return True
1099 else:
1100 utils.err("Unable to find constraint - '%s'" % c_id, False)
1101 if returnStatus:
1102 return False
1103 sys.exit(1)
1104 return None
1105
1106
1107 def _split_set_constraints(
1108 constraints_dto: CibConstraintsDto,
1109 ) -> tuple[CibConstraintsDto, CibConstraintsDto]:
1110 return (
1111 CibConstraintsDto(
1112 location=constraints_dto.location,
1113 colocation=constraints_dto.colocation,
1114 order=constraints_dto.order,
1115 ticket=constraints_dto.ticket,
1116 ),
1117 CibConstraintsDto(
1118 location_set=constraints_dto.location_set,
1119 colocation_set=constraints_dto.colocation_set,
1120 order_set=constraints_dto.order_set,
1121 ticket_set=constraints_dto.ticket_set,
1122 ),
1123 )
1124
1125
1126 def _find_constraints_containing_resource(
1127 resources_dto: CibResourcesDto,
1128 constraints_dto: CibConstraintsDto,
1129 resource_id: str,
1130 ) -> CibConstraintsDto:
1131 resources_filter = [resource_id]
1132 # Original implementation only included parent resource only if resource_id
1133 # was referring to a primitive resource, ignoring groups. This may change in
1134 # the future if necessary.
1135 if any(
1136 primitive_dto.id == resource_id
1137 for primitive_dto in resources_dto.primitives
1138 ):
1139 for clone_dto in resources_dto.clones:
1140 if clone_dto.member_id == resource_id:
1141 resources_filter.append(clone_dto.id)
1142 break
1143 return _filter_constraints_by_resources(
1144 constraints_dto, resources_filter, []
1145 )
1146
1147
1148 def ref(
1149 lib: Any, argv: list[str], modifiers: parse_args.InputModifiers
1150 ) -> None:
1151 modifiers.ensure_only_supported("-f")
1152 if not argv:
1153 raise CmdLineInputError()
1154
1155 resources_dto = cast(
1156 CibResourcesDto, lib.resource.get_configured_resources()
1157 )
1158
1159 constraints_dto = cast(
1160 CibConstraintsDto,
1161 lib.constraint.get_config(evaluate_rules=False),
1162 )
1163
1164 for resource_id in sorted(set(argv)):
1165 constraint_ids = get_all_constraints_ids(
1166 _find_constraints_containing_resource(
1167 resources_dto, constraints_dto, resource_id
1168 )
1169 )
1170 print(f"Resource: {resource_id}")
1171 if constraint_ids:
1172 print(
1173 "\n".join(
1174 indent(
1175 sorted(constraint_ids),
1176 indent_step=INDENT_STEP,
1177 )
1178 )
1179 )
1180 else:
1181 print(" No Matches")
1182
1183
1184 def remove_constraints_containing(
1185 resource_id: str, output=False, constraints_element=None, passed_dom=None
1186 ):
1187 """
1188 Commandline options:
1189 * -f - CIB file, effective only if passed_dom is None
1190 """
1191 lib = utils.get_library_wrapper()
1192 modifiers = utils.get_input_modifiers()
1193 resources_dto = cast(
1194 CibResourcesDto, lib.resource.get_configured_resources()
1195 )
1196
1197 constraints_dto, set_constraints_dto = _split_set_constraints(
1198 cast(
1199 CibConstraintsDto,
1200 lib.constraint.get_config(evaluate_rules=False),
1201 )
1202 )
1203 constraints = sorted(
1204 get_all_constraints_ids(
1205 _find_constraints_containing_resource(
1206 resources_dto, constraints_dto, resource_id
1207 )
1208 )
1209 )
1210 set_constraints = sorted(
1211 get_all_constraints_ids(
1212 _find_constraints_containing_resource(
1213 resources_dto, set_constraints_dto, resource_id
1214 )
1215 )
1216 )
1217 for c in constraints:
1218 if output:
1219 print_to_stderr(f"Removing Constraint - {c}")
1220 if constraints_element is not None:
1221 constraint_rm(
1222 lib,
1223 [c],
1224 modifiers,
1225 True,
1226 constraints_element,
1227 passed_dom=passed_dom,
1228 )
1229 else:
1230 constraint_rm(lib, [c], modifiers, passed_dom=passed_dom)
1231
1232 if set_constraints:
1233 (dom, constraintsElement) = getCurrentConstraints(passed_dom)
1234 for set_c in constraintsElement.getElementsByTagName("resource_ref")[:]:
1235 # If resource id is in a set, remove it from the set, if the set
1236 # is empty, then we remove the set, if the parent of the set
1237 # is empty then we remove it
1238 if set_c.getAttribute("id") == resource_id:
1239 parent_node = set_c.parentNode
1240 parent_node.removeChild(set_c)
1241 if output:
1242 print_to_stderr(
1243 "Removing {} from set {}".format(
1244 resource_id, parent_node.getAttribute("id")
1245 )
1246 )
1247 if parent_node.getElementsByTagName("resource_ref").length == 0:
1248 print_to_stderr(
1249 "Removing set {}".format(parent_node.getAttribute("id"))
1250 )
1251 parent_node_2 = parent_node.parentNode
1252 parent_node_2.removeChild(parent_node)
1253 if (
1254 parent_node_2.getElementsByTagName(
1255 "resource_set"
1256 ).length
1257 == 0
1258 ):
1259 parent_node_2.parentNode.removeChild(parent_node_2)
1260 print_to_stderr(
1261 "Removing constraint {}".format(
1262 parent_node_2.getAttribute("id")
1263 )
1264 )
1265 if passed_dom:
1266 return dom
1267 utils.replace_cib_configuration(dom)
1268 return None
1269
1270
1271 # Re-assign any constraints referencing a resource to its parent (a clone
1272 # or master)
1273 def constraint_resource_update(old_id, dom):
1274 """
1275 Commandline options: no options
1276 """
1277 new_id = None
1278 clone_ms_parent = utils.dom_get_resource_clone_ms_parent(dom, old_id)
1279 if clone_ms_parent:
1280 new_id = clone_ms_parent.getAttribute("id")
1281
1282 if new_id:
1283 constraints = dom.getElementsByTagName("rsc_location")
1284 constraints += dom.getElementsByTagName("rsc_order")
1285 constraints += dom.getElementsByTagName("rsc_colocation")
1286 attrs_to_update = ["rsc", "first", "then", "with-rsc"]
1287 for constraint in constraints:
1288 for attr in attrs_to_update:
1289 if constraint.getAttribute(attr) == old_id:
1290 constraint.setAttribute(attr, new_id)
1291 return dom
1292