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