1 import contextlib
2 import datetime
3 import json
4 import math
5 import os
6 import subprocess
7 import sys
8 import tempfile
9 import time
10 import xml.dom.minidom
11 from collections.abc import Callable, Iterable, Mapping
12 from typing import Any, cast
13 from xml.parsers.expat import ExpatError
14
15 import pcs.lib.pacemaker.live as lib_pacemaker
16 from pcs import settings, utils
17 from pcs.cli.common import parse_args
18 from pcs.cli.common.errors import (
19 ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE,
20 CmdLineInputError,
21 )
22 from pcs.cli.common.parse_args import (
23 OUTPUT_FORMAT_VALUE_CMD,
24 OUTPUT_FORMAT_VALUE_JSON,
25 Argv,
26 InputModifiers,
27 KeyValueParser,
28 )
29 from pcs.cli.common.tools import print_to_stderr
30 from pcs.cli.file import metadata as file_metadata
31 from pcs.cli.reports import process_library_reports
32 from pcs.cli.reports.messages import report_item_msg_from_dto
33 from pcs.cli.reports.output import deprecation_warning, warn
34 from pcs.common import file as pcs_file
35 from pcs.common import file_type_codes, reports
36 from pcs.common.auth import HostAuthData
37 from pcs.common.corosync_conf import CorosyncConfDto, CorosyncNodeDto
38 from pcs.common.file import RawFileError
39 from pcs.common.host import Destination
40 from pcs.common.interface import dto
41 from pcs.common.node_communicator import HostNotFound, Request, RequestData
42 from pcs.common.str_tools import format_list, indent, join_multilines
43 from pcs.common.tools import format_os_error
44 from pcs.common.types import StringCollection, StringIterable
45 from pcs.lib.commands.remote_node import _destroy_pcmk_remote_env
46 from pcs.lib.communication.nodes import CheckAuth
47 from pcs.lib.communication.pcs_cfgsync import SetConfigs
48 from pcs.lib.communication.tools import RunRemotelyBase, run_and_raise
49 from pcs.lib.communication.tools import run as run_com_cmd
50 from pcs.lib.corosync import qdevice_net
51 from pcs.lib.corosync.live import QuorumStatusException, QuorumStatusFacade
52 from pcs.lib.errors import LibraryError
53 from pcs.lib.file.instance import FileInstance
54 from pcs.lib.file.raw_file import raw_file_error_report
55 from pcs.lib.node import get_existing_nodes_names
56 from pcs.lib.pcs_cfgsync.const import SYNCED_CONFIGS
57 from pcs.utils import parallel_for_nodes
58
59
60 def _corosync_conf_local_cmd_call(
61 corosync_conf_path: parse_args.ModifierValueType,
62 lib_cmd: Callable[[bytes], bytes],
63 ) -> None:
64 """
65 Call a library command that requires modifications of a corosync.conf file
66 supplied as an argument
67
68 The lib command needs to take the corosync.conf file content as its first
69 argument
70
71 lib_cmd -- the lib command to be called
72 """
73 corosync_conf_file = pcs_file.RawFile(
74 file_metadata.for_file_type(
75 file_type_codes.COROSYNC_CONF, corosync_conf_path
76 )
77 )
78
79 try:
80 corosync_conf_file.write(
81 lib_cmd(
82 corosync_conf_file.read(),
83 ),
84 can_overwrite=True,
85 )
86 except pcs_file.RawFileError as e:
87 raise CmdLineInputError(
88 reports.messages.FileIoError(
89 e.metadata.file_type_code,
90 e.action,
91 e.reason,
92 file_path=e.metadata.path,
93 ).message
94 ) from e
95
96
97 def cluster_cib_upgrade_cmd(
98 lib: Any, argv: Argv, modifiers: InputModifiers
99 ) -> None:
100 """
101 Options:
102 * -f - CIB file
103 """
104 del lib
105 modifiers.ensure_only_supported("-f")
106 if argv:
107 raise CmdLineInputError()
108 utils.cluster_upgrade()
109
110
111 def cluster_disable_cmd(
112 lib: Any, argv: Argv, modifiers: InputModifiers
113 ) -> None:
114 """
115 Options:
116 * --all - disable all cluster nodes
117 * --request-timeout - timeout for HTTP requests - effective only when at
118 least one node has been specified or --all has been used
119 """
120 del lib
121 modifiers.ensure_only_supported("--all", "--request-timeout")
122 if modifiers.get("--all"):
123 if argv:
124 utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE)
125 disable_cluster_all()
126 else:
127 disable_cluster(argv)
128
129
130 def cluster_enable_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
131 """
132 Options:
133 * --all - enable all cluster nodes
134 * --request-timeout - timeout for HTTP requests - effective only when at
135 least one node has been specified or --all has been used
136 """
137 del lib
138 modifiers.ensure_only_supported("--all", "--request-timeout")
139 if modifiers.get("--all"):
140 if argv:
141 utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE)
142 enable_cluster_all()
143 else:
144 enable_cluster(argv)
145
146
147 def cluster_stop_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
148 """
149 Options:
150 * --force - no error when possible quorum loss
151 * --request-timeout - timeout for HTTP requests - effective only when at
152 least one node has been specified
153 * --pacemaker - stop pacemaker, only effective when no node has been
154 specified
155 * --corosync - stop corosync, only effective when no node has been
156 specified
157 * --all - stop all cluster nodes
158 """
159 del lib
160 modifiers.ensure_only_supported(
161 "--wait",
162 "--request-timeout",
163 "--pacemaker",
164 "--corosync",
165 "--all",
166 "--force",
167 )
168 if modifiers.get("--all"):
169 if argv:
170 utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE)
171 stop_cluster_all()
172 else:
173 stop_cluster(argv)
174
175
176 def cluster_start_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
177 """
178 Options:
179 * --wait
180 * --request-timeout - timeout for HTTP requests, have effect only if at
181 least one node have been specified
182 * --all - start all cluster nodes
183 """
184 del lib
185 modifiers.ensure_only_supported(
186 "--wait", "--request-timeout", "--all", "--corosync_conf"
187 )
188 if modifiers.get("--all"):
189 if argv:
190 utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE)
191 start_cluster_all()
192 else:
193 start_cluster(argv)
194
195
196 def authkey_corosync(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
197 """
198 Options:
199 * --force - skip check for authkey length
200 * --request-timeout - timeout for HTTP requests
201 * --skip-offline - skip unreachable nodes
202 """
203 modifiers.ensure_only_supported(
204 "--force", "--skip-offline", "--request-timeout"
205 )
206 if len(argv) > 1:
207 raise CmdLineInputError()
208 force_flags = []
209 if modifiers.get("--force"):
210 force_flags.append(reports.codes.FORCE)
211 if modifiers.get("--skip-offline"):
212 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
213 corosync_authkey = None
214 if argv:
215 try:
216 with open(argv[0], "rb") as file:
217 corosync_authkey = file.read()
218 except OSError as e:
219 utils.err(f"Unable to read file '{argv[0]}': {format_os_error(e)}")
220 lib.cluster.corosync_authkey_change(
221 corosync_authkey=corosync_authkey,
222 force_flags=force_flags,
223 )
224
225
226 def sync_nodes(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
227 """
228 Options:
229 * --request-timeout - timeout for HTTP requests
230 """
231 del lib
232 modifiers.ensure_only_supported("--request-timeout")
233 if argv:
234 raise CmdLineInputError()
235
236 config = utils.getCorosyncConf()
237 nodes, report_list = get_existing_nodes_names(
238 utils.get_corosync_conf_facade(conf_text=config)
239 )
240 if not nodes:
241 report_list.append(
242 reports.ReportItem.error(
243 reports.messages.CorosyncConfigNoNodesDefined()
244 )
245 )
246 if report_list:
247 process_library_reports(report_list)
248
249 for node in nodes:
250 utils.setCorosyncConfig(node, config)
251
252 warn(
253 "Corosync configuration has been synchronized, please reload corosync "
254 "daemon using 'pcs cluster reload corosync' command."
255 )
256
257
258 def start_cluster(argv: Argv) -> None:
259 """
260 Commandline options:
261 * --wait
262 * --request-timeout - timeout for HTTP requests, have effect only if at
263 least one node have been specified
264 """
265 wait = False
266 wait_timeout = None
267 if "--wait" in utils.pcs_options:
268 wait_timeout = utils.validate_wait_get_timeout(False)
269 wait = True
270
271 if argv:
272 nodes = set(argv) # unique
273 start_cluster_nodes(nodes)
274 if wait:
275 wait_for_nodes_started(nodes, wait_timeout)
276 return
277
278 if not utils.hasCorosyncConf():
279 utils.err("cluster is not currently configured on this node")
280
281 print_to_stderr("Starting Cluster...")
282 service_list = ["corosync"]
283 if utils.need_to_handle_qdevice_service():
284 service_list.append("corosync-qdevice")
285 service_list.append("pacemaker")
286 for service in service_list:
287 utils.start_service(service)
288 if wait:
289 wait_for_nodes_started([], wait_timeout)
290
291
292 def start_cluster_all() -> None:
293 """
294 Commandline options:
295 * --wait
296 * --request-timeout - timeout for HTTP requests
297 """
298 wait = False
299 wait_timeout = None
300 if "--wait" in utils.pcs_options:
301 wait_timeout = utils.validate_wait_get_timeout(False)
302 wait = True
303
304 all_nodes, report_list = get_existing_nodes_names(
305 utils.get_corosync_conf_facade()
306 )
307 if not all_nodes:
308 report_list.append(
309 reports.ReportItem.error(
310 reports.messages.CorosyncConfigNoNodesDefined()
311 )
312 )
313 if report_list:
314 process_library_reports(report_list)
315
316 start_cluster_nodes(all_nodes)
317 if wait:
318 wait_for_nodes_started(all_nodes, wait_timeout)
319
320
321 def start_cluster_nodes(nodes: StringCollection) -> None:
322 """
323 Commandline options:
324 * --request-timeout - timeout for HTTP requests
325 """
326 # Large clusters take longer time to start up. So we make the timeout longer
327 # for each 8 nodes:
328 # 1 - 8 nodes: 1 * timeout
329 # 9 - 16 nodes: 2 * timeout
330 # 17 - 24 nodes: 3 * timeout
331 # and so on
332 # Users can override this and set their own timeout by specifying
333 # the --request-timeout option (see utils.sendHTTPRequest).
334 timeout = int(
335 settings.default_request_timeout * math.ceil(len(nodes) / 8.0)
336 )
337 utils.read_known_hosts_file() # cache known hosts
338 node_errors = parallel_for_nodes(
339 utils.startCluster, nodes, quiet=True, timeout=timeout
340 )
341 if node_errors:
342 utils.err(
343 "unable to start all nodes\n" + "\n".join(node_errors.values())
344 )
345
346
347 def is_node_fully_started(node_status) -> bool:
348 """
349 Commandline options: no options
350 """
351 return (
352 "online" in node_status
353 and "pending" in node_status
354 and node_status["online"]
355 and not node_status["pending"]
356 )
357
358
359 def wait_for_local_node_started(
360 stop_at: datetime.datetime, interval: float
361 ) -> tuple[int, str]:
362 """
363 Commandline options: no options
364 """
365 try:
366 while True:
367 time.sleep(interval)
368 node_status = lib_pacemaker.get_local_node_status(
369 utils.cmd_runner()
370 )
371 if is_node_fully_started(node_status):
372 return 0, "Started"
373 if datetime.datetime.now() > stop_at:
374 return 1, "Waiting timeout"
375 except LibraryError as e:
376 return (
377 1,
378 "Unable to get node status: {0}".format(
379 "\n".join(
380 report_item_msg_from_dto(
381 cast(reports.ReportItemDto, item).message
382 ).message
383 for item in e.args
384 )
385 ),
386 )
387
388
389 def wait_for_remote_node_started(
390 node: str, stop_at: datetime.datetime, interval: float
391 ) -> tuple[int, str]:
392 """
393 Commandline options:
394 * --request-timeout - timeout for HTTP requests
395 """
396 while True:
397 time.sleep(interval)
398 code, output = utils.getPacemakerNodeStatus(node)
399 # HTTP error, permission denied or unable to auth
400 # there is no point in trying again as it won't get magically fixed
401 if code in [1, 3, 4]:
402 return 1, output
403 if code == 0:
404 try:
405 node_status = json.loads(output)
406 if is_node_fully_started(node_status):
407 return 0, "Started"
408 except (ValueError, KeyError):
409 # this won't get fixed either
410 return 1, "Unable to get node status"
411 if datetime.datetime.now() > stop_at:
412 return 1, "Waiting timeout"
413
414
415 def wait_for_nodes_started(
416 node_list: StringIterable, timeout: int | None = None
417 ) -> None:
418 """
419 Commandline options:
420 * --request-timeout - timeout for HTTP request, effective only if
421 node_list is not empty list
422 """
423 timeout = 60 * 15 if timeout is None else timeout
424 interval = 2
425 stop_at = datetime.datetime.now() + datetime.timedelta(seconds=timeout)
426 print_to_stderr("Waiting for node(s) to start...")
427 if not node_list:
428 code, output = wait_for_local_node_started(stop_at, interval)
429 if code != 0:
430 utils.err(output)
431 else:
432 print_to_stderr(output)
433 else:
434 utils.read_known_hosts_file() # cache known hosts
435 node_errors = parallel_for_nodes(
436 wait_for_remote_node_started, node_list, stop_at, interval
437 )
438 if node_errors:
439 utils.err("unable to verify all nodes have started")
440
441
442 def stop_cluster_all() -> None:
443 """
444 Commandline options:
445 * --force - no error when possible quorum loss
446 * --request-timeout - timeout for HTTP requests
447 """
448 all_nodes, report_list = get_existing_nodes_names(
449 utils.get_corosync_conf_facade()
450 )
451 if not all_nodes:
452 report_list.append(
453 reports.ReportItem.error(
454 reports.messages.CorosyncConfigNoNodesDefined()
455 )
456 )
457 if report_list:
458 process_library_reports(report_list)
459
460 stop_cluster_nodes(all_nodes)
461
462
463 def stop_cluster_nodes(nodes: StringCollection) -> None: # noqa: PLR0912
464 """
465 Commandline options:
466 * --force - no error when possible quorum loss
467 * --request-timeout - timeout for HTTP requests
468 """
469 all_nodes, report_list = get_existing_nodes_names(
470 utils.get_corosync_conf_facade()
471 )
472 unknown_nodes = set(nodes) - set(all_nodes)
473 if unknown_nodes:
474 if report_list:
475 process_library_reports(report_list)
476 utils.err(
477 "nodes '%s' do not appear to exist in configuration"
478 % "', '".join(sorted(unknown_nodes))
479 )
480
481 utils.read_known_hosts_file() # cache known hosts
482 stopping_all = set(nodes) >= set(all_nodes)
483 if "--force" not in utils.pcs_options and not stopping_all:
484 error_list = []
485 for node in nodes:
486 retval, data = utils.get_remote_quorumtool_output(node)
487 if retval != 0:
488 error_list.append(node + ": " + data)
489 continue
490 try:
491 quorum_status_facade = QuorumStatusFacade.from_string(data)
492 if not quorum_status_facade.is_quorate:
493 # Get quorum status from a quorate node, non-quorate nodes
494 # may provide inaccurate info. If no node is quorate, there
495 # is no quorum to be lost and therefore no error to be
496 # reported.
497 continue
498 if quorum_status_facade.stopping_nodes_cause_quorum_loss(nodes):
499 utils.err(
500 "Stopping the node(s) will cause a loss of the quorum"
501 + ", use --force to override"
502 )
503 else:
504 # We have the info, no need to print errors
505 error_list = []
506 break
507 except QuorumStatusException:
508 if not utils.is_node_offline_by_quorumtool_output(data):
509 error_list.append(node + ": Unable to get quorum status")
510 # else the node seems to be stopped already
511 if error_list:
512 utils.err(
513 "Unable to determine whether stopping the nodes will cause "
514 + "a loss of the quorum, use --force to override\n"
515 + "\n".join(error_list)
516 )
517
518 was_error = False
519 node_errors = parallel_for_nodes(
520 utils.repeat_if_timeout(utils.stopPacemaker), nodes, quiet=True
521 )
522 accessible_nodes = [node for node in nodes if node not in node_errors]
523 if node_errors:
524 utils.err(
525 "unable to stop all nodes\n" + "\n".join(node_errors.values()),
526 exit_after_error=not accessible_nodes,
527 )
528 was_error = True
529
530 for node in node_errors:
531 print_to_stderr(
532 "{0}: Not stopping cluster - node is unreachable".format(node)
533 )
534
535 node_errors = parallel_for_nodes(
536 utils.stopCorosync, accessible_nodes, quiet=True
537 )
538 if node_errors:
539 utils.err(
540 "unable to stop all nodes\n" + "\n".join(node_errors.values())
541 )
542 if was_error:
543 utils.err("unable to stop all nodes")
544
545
546 def enable_cluster(argv: Argv) -> None:
547 """
548 Commandline options:
549 * --request-timeout - timeout for HTTP requests, effective only if at
550 least one node has been specified
551 """
552 if argv:
553 enable_cluster_nodes(argv)
554 return
555
556 try:
557 utils.enableServices()
558 except LibraryError as e:
559 process_library_reports(list(e.args))
560
561
562 def disable_cluster(argv: Argv) -> None:
563 """
564 Commandline options:
565 * --request-timeout - timeout for HTTP requests, effective only if at
566 least one node has been specified
567 """
568 if argv:
569 disable_cluster_nodes(argv)
570 return
571
572 try:
573 utils.disableServices()
574 except LibraryError as e:
575 process_library_reports(list(e.args))
576
577
578 def enable_cluster_all() -> None:
579 """
580 Commandline options:
581 * --request-timeout - timeout for HTTP requests
582 """
583 all_nodes, report_list = get_existing_nodes_names(
584 utils.get_corosync_conf_facade()
585 )
586 if not all_nodes:
587 report_list.append(
588 reports.ReportItem.error(
589 reports.messages.CorosyncConfigNoNodesDefined()
590 )
591 )
592 if report_list:
593 process_library_reports(report_list)
594
595 enable_cluster_nodes(all_nodes)
596
597
598 def disable_cluster_all() -> None:
599 """
600 Commandline options:
601 * --request-timeout - timeout for HTTP requests
602 """
603 all_nodes, report_list = get_existing_nodes_names(
604 utils.get_corosync_conf_facade()
605 )
606 if not all_nodes:
607 report_list.append(
608 reports.ReportItem.error(
609 reports.messages.CorosyncConfigNoNodesDefined()
610 )
611 )
612 if report_list:
613 process_library_reports(report_list)
614
615 disable_cluster_nodes(all_nodes)
616
617
618 def enable_cluster_nodes(nodes: StringIterable) -> None:
619 """
620 Commandline options:
621 * --request-timeout - timeout for HTTP requests
622 """
623 error_list = utils.map_for_error_list(utils.enableCluster, nodes)
624 if error_list:
625 utils.err("unable to enable all nodes\n" + "\n".join(error_list))
626
627
628 def disable_cluster_nodes(nodes: StringIterable) -> None:
629 """
630 Commandline options:
631 * --request-timeout - timeout for HTTP requests
632 """
633 error_list = utils.map_for_error_list(utils.disableCluster, nodes)
634 if error_list:
635 utils.err("unable to disable all nodes\n" + "\n".join(error_list))
636
637
638 def destroy_cluster(argv: Argv) -> None:
639 """
640 Commandline options:
641 * --request-timeout - timeout for HTTP requests
642 """
643 if argv:
644 utils.read_known_hosts_file() # cache known hosts
645 # stop pacemaker and resources while cluster is still quorate
646 nodes = argv
647 node_errors = parallel_for_nodes(
648 utils.repeat_if_timeout(utils.stopPacemaker), nodes, quiet=True
649 )
650 # proceed with destroy regardless of errors
651 # destroy will stop any remaining cluster daemons
652 node_errors = parallel_for_nodes(
653 utils.destroyCluster, nodes, quiet=True
654 )
655 if node_errors:
656 utils.err(
657 "unable to destroy cluster\n" + "\n".join(node_errors.values())
658 )
659
660
661 def stop_cluster(argv: Argv) -> None:
662 """
663 Commandline options:
664 * --force - no error when possible quorum loss
665 * --request-timeout - timeout for HTTP requests - effective only when at
666 least one node has been specified
667 * --pacemaker - stop pacemaker, only effective when no node has been
668 specified
669 """
670 if argv:
671 stop_cluster_nodes(argv)
672 return
673
674 if "--force" not in utils.pcs_options:
675 # corosync 3.0.1 and older:
676 # - retval is 0 on success if a node is not in a partition with quorum
677 # - retval is 1 on error OR on success if a node has quorum
678 # corosync 3.0.2 and newer:
679 # - retval is 0 on success if a node has quorum
680 # - retval is 1 on error
681 # - retval is 2 on success if a node is not in a partition with quorum
682 output, dummy_retval = utils.run(["corosync-quorumtool", "-p", "-s"])
683 try:
684 if QuorumStatusFacade.from_string(
685 output
686 ).stopping_local_node_cause_quorum_loss():
687 utils.err(
688 "Stopping the node will cause a loss of the quorum"
689 + ", use --force to override"
690 )
691 except QuorumStatusException:
692 if not utils.is_node_offline_by_quorumtool_output(output):
693 utils.err(
694 "Unable to determine whether stopping the node will cause "
695 + "a loss of the quorum, use --force to override"
696 )
697 # else the node seems to be stopped already, proceed to be sure
698
699 stop_all = (
700 "--pacemaker" not in utils.pcs_options
701 and "--corosync" not in utils.pcs_options
702 )
703 if stop_all or "--pacemaker" in utils.pcs_options:
704 stop_cluster_pacemaker()
705 if stop_all or "--corosync" in utils.pcs_options:
706 stop_cluster_corosync()
707
708
709 def stop_cluster_pacemaker() -> None:
710 """
711 Commandline options: no options
712 """
713 print_to_stderr("Stopping Cluster (pacemaker)...")
714 utils.stop_service("pacemaker")
715
716
717 def stop_cluster_corosync() -> None:
718 """
719 Commandline options: no options
720 """
721 print_to_stderr("Stopping Cluster (corosync)...")
722 service_list = []
723 if utils.need_to_handle_qdevice_service():
724 service_list.append("corosync-qdevice")
725 service_list.append("corosync")
726 for service in service_list:
727 utils.stop_service(service)
728
729
730 def kill_cluster(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
731 """
732 Options: no options
733 """
734 del lib
735 if argv:
736 raise CmdLineInputError()
737 modifiers.ensure_only_supported()
738 dummy_output, dummy_retval = kill_local_cluster_services()
739
740
741 # if dummy_retval != 0:
742 # print "Error: unable to execute killall -9"
743 # print output
744 # sys.exit(1)
745
746
747 def kill_local_cluster_services() -> tuple[str, int]:
748 """
749 Commandline options: no options
750 """
751 all_cluster_daemons = [
752 # Daemons taken from cluster-clean script in pacemaker
753 "pacemaker-attrd",
754 "pacemaker-based",
755 "pacemaker-controld",
756 "pacemaker-execd",
757 "pacemaker-fenced",
758 "pacemaker-remoted",
759 "pacemaker-schedulerd",
760 "pacemakerd",
761 "dlm_controld",
762 "gfs_controld",
763 # Corosync daemons
764 "corosync-qdevice",
765 "corosync",
766 ]
767 return utils.run([settings.killall_exec, "-9"] + all_cluster_daemons)
768
769
770 def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912, PLR0915
771 """
772 Options:
773 * --wait
774 * --config - push only configuration section of CIB
775 * -f - CIB file
776 """
777
778 def get_details_from_crm_verify():
779 # get a new runner to run crm_verify command and pass the CIB filename
780 # into it so that the verify is run on the file instead on the live
781 # cluster CIB
782 verify_runner = utils.cmd_runner(cib_file_override=filename)
783 # Request verbose output, otherwise we may only get an unhelpful
784 # message:
785 # Configuration invalid (with errors) (-V may provide more detail)
786 # verify_returncode is always expected to be non-zero to indicate
787 # invalid CIB - ve run the verify because the CIB is invalid
788 (
789 verify_stdout,
790 verify_stderr,
791 verify_returncode,
792 verify_can_be_more_verbose,
793 ) = lib_pacemaker.verify(verify_runner, verbose=True)
794 return join_multilines([verify_stdout, verify_stderr])
795
796 del lib
797 modifiers.ensure_only_supported("--wait", "--config", "-f")
798 if len(argv) > 2:
799 raise CmdLineInputError()
800
801 filename = None
802 scope = None
803 timeout = None
804 diff_against = None
805
806 if modifiers.get("--wait"):
807 timeout = utils.validate_wait_get_timeout()
808 for arg in argv:
809 if "=" not in arg:
810 filename = arg
811 else:
812 arg_name, arg_value = arg.split("=", 1)
813 if arg_name == "scope":
814 if modifiers.get("--config"):
815 utils.err("Cannot use both scope and --config")
816 if not utils.is_valid_cib_scope(arg_value):
817 utils.err("invalid CIB scope '%s'" % arg_value)
818 else:
819 scope = arg_value
820 elif arg_name == "diff-against":
821 diff_against = arg_value
822 else:
823 raise CmdLineInputError()
824 if modifiers.get("--config"):
825 scope = "configuration"
826 if diff_against and scope:
827 utils.err("Cannot use both scope and diff-against")
828 if not filename:
829 raise CmdLineInputError()
830
831 try:
|
CID (unavailable; MK=26a3af0897e6812f1acaacd47e7ce85f) (#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. |
832 new_cib_dom = xml.dom.minidom.parse(filename)
833 if scope and not new_cib_dom.getElementsByTagName(scope):
834 utils.err(
835 "unable to push cib, scope '%s' not present in new cib" % scope
836 )
837 except (OSError, ExpatError) as e:
838 utils.err("unable to parse new cib: %s" % e)
839
840 EXITCODE_INVALID_CIB = 78
841 runner = utils.cmd_runner()
842
843 if diff_against:
844 command = [
845 settings.crm_diff_exec,
846 "--original",
847 diff_against,
848 "--new",
849 filename,
850 "--no-version",
851 ]
852 patch, stderr, retval = runner.run(command)
853 # 0 (CRM_EX_OK) - success with no difference
854 # 1 (CRM_EX_ERROR) - success with difference
855 # 64 (CRM_EX_USAGE) - usage error
856 # 65 (CRM_EX_DATAERR) - XML fragments not parseable
857 if retval > 1:
858 utils.err("unable to diff the CIBs:\n" + stderr)
859 if retval == 0:
860 print_to_stderr(
861 "The new CIB is the same as the original CIB, nothing to push."
862 )
863 sys.exit(0)
864
865 command = [
866 settings.cibadmin_exec,
867 "--patch",
868 "--xml-pipe",
869 ]
870 output, stderr, retval = runner.run(command, patch)
871 if retval != 0:
872 push_output = stderr + output
873 verify_output = (
874 get_details_from_crm_verify()
875 if retval == EXITCODE_INVALID_CIB
876 else ""
877 )
878 error_text = (
879 f"{push_output}\n\n{verify_output}"
880 if verify_output.strip()
881 else push_output
882 )
883 utils.err("unable to push cib\n" + error_text)
884
885 else:
886 command = ["cibadmin", "--replace", "--xml-file", filename]
887 if scope:
888 command.append("--scope=%s" % scope)
889 output, retval = utils.run(command)
890 # 103 (CRM_EX_OLD) - update older than existing config
891 if retval == 103:
892 utils.err(
893 "Unable to push to the CIB because pushed configuration "
894 "is older than existing one. If you are sure you want to "
895 "push this configuration, try to use --config to replace only "
896 "configuration part instead of whole CIB. Otherwise get current"
897 " configuration by running command 'pcs cluster cib' and update"
898 " that."
899 )
900 elif retval != 0:
901 verify_output = (
902 get_details_from_crm_verify()
903 if retval == EXITCODE_INVALID_CIB
904 else ""
905 )
906 error_text = (
907 f"{output}\n\n{verify_output}"
908 if verify_output.strip()
909 else output
910 )
911 utils.err("unable to push cib\n" + error_text)
912
913 print_to_stderr("CIB updated")
914 try:
915 cib_errors = lib_pacemaker.get_cib_verification_errors(runner)
916 if cib_errors:
917 print_to_stderr("\n".join(cib_errors))
918 except lib_pacemaker.BadApiResultFormat as e:
919 print_to_stderr(
920 f"Unable to verify CIB: {e.original_exception}\n"
921 f"crm_verify output:\n{e.pacemaker_response}"
922 )
923
924 if not modifiers.is_specified("--wait"):
925 return
926 cmd = ["crm_resource", "--wait"]
927 if timeout:
928 cmd.extend(["--timeout", str(timeout)])
929 output, retval = utils.run(cmd)
930 if retval != 0:
931 msg = []
932 if retval == settings.pacemaker_wait_timeout_status:
933 msg.append("waiting timeout")
934 if output:
935 msg.append("\n" + output)
936 utils.err("\n".join(msg).strip())
937
938
939 def cluster_edit(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
940 """
941 Options:
942 * --config - edit configuration section of CIB
943 * -f - CIB file
944 * --wait
945 """
946 modifiers.ensure_only_supported("--config", "--wait", "-f")
947 if "EDITOR" in os.environ:
948 if len(argv) > 1:
949 raise CmdLineInputError()
950
951 scope = None
952 scope_arg = ""
953 for arg in argv:
954 if "=" not in arg:
955 raise CmdLineInputError()
956 arg_name, arg_value = arg.split("=", 1)
957 if arg_name == "scope" and not modifiers.get("--config"):
958 if not utils.is_valid_cib_scope(arg_value):
959 utils.err("invalid CIB scope '%s'" % arg_value)
960 else:
961 scope_arg = arg
962 scope = arg_value
963 else:
964 raise CmdLineInputError()
965 if modifiers.get("--config"):
966 scope = "configuration"
967 # Leave scope_arg empty as cluster_push will pick up a --config
968 # option from utils.pcs_options
969 scope_arg = ""
970
971 editor = os.environ["EDITOR"]
972 cib = utils.get_cib(scope)
973 with tempfile.NamedTemporaryFile(mode="w+", suffix=".pcs") as tempcib:
974 tempcib.write(cib)
975 tempcib.flush()
976 try:
977 subprocess.call([editor, tempcib.name])
978 except OSError:
979 utils.err("unable to open file with $EDITOR: " + editor)
980
981 tempcib.seek(0)
982 newcib = "".join(tempcib.readlines())
983 if newcib == cib:
984 print_to_stderr("CIB not updated, no changes detected")
985 else:
986 cluster_push(
987 lib,
988 [arg for arg in [tempcib.name, scope_arg] if arg],
989 modifiers.get_subset("--wait", "--config", "-f"),
990 )
991
992 else:
993 utils.err("$EDITOR environment variable is not set")
994
995
996 def get_cib(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
997 """
998 Options:
999 * --config show configuration section of CIB
1000 * -f - CIB file
1001 """
1002 del lib
1003 modifiers.ensure_only_supported("--config", "-f")
1004 if len(argv) > 2:
1005 raise CmdLineInputError()
1006
1007 filename = None
1008 scope = None
1009 for arg in argv:
1010 if "=" not in arg:
1011 filename = arg
1012 else:
1013 arg_name, arg_value = arg.split("=", 1)
1014 if arg_name == "scope" and not modifiers.get("--config"):
1015 if not utils.is_valid_cib_scope(arg_value):
1016 utils.err("invalid CIB scope '%s'" % arg_value)
1017 else:
1018 scope = arg_value
1019 else:
1020 raise CmdLineInputError()
1021 if modifiers.get("--config"):
1022 scope = "configuration"
1023
1024 if not filename:
1025 print(utils.get_cib(scope).rstrip())
1026 else:
1027 output = utils.get_cib(scope)
1028 if not output:
1029 utils.err("No data in the CIB")
1030 try:
1031 with open(filename, "w") as cib_file:
1032 cib_file.write(output)
1033 except OSError as e:
1034 utils.err(
1035 "Unable to write to file '%s', %s" % (filename, e.strerror)
1036 )
1037
1038
1039 class RemoteAddNodes(RunRemotelyBase):
1040 def __init__(self, report_processor, target, data):
1041 super().__init__(report_processor)
1042 self._target = target
1043 self._data = data
1044 self._success = False
1045
1046 def get_initial_request_list(self):
1047 return [
1048 Request(
1049 self._target,
1050 RequestData(
1051 "remote/cluster_add_nodes",
1052 [("data_json", json.dumps(self._data))],
1053 ),
1054 )
1055 ]
1056
1057 def _process_response(self, response):
1058 node_label = response.request.target.label
1059 report_item = self._get_response_report(response)
1060 if report_item is not None:
1061 self._report(report_item)
1062 return
1063
1064 try:
1065 output = json.loads(response.data)
1066 for report_dict in output["report_list"]:
1067 self._report(
1068 reports.ReportItem(
1069 severity=reports.ReportItemSeverity(
1070 report_dict["severity"],
1071 report_dict["forceable"],
1072 ),
1073 message=reports.messages.LegacyCommonMessage(
1074 report_dict["code"],
1075 report_dict["info"],
1076 report_dict["report_text"],
1077 ),
1078 )
1079 )
1080 if output["status"] == "success":
1081 self._success = True
1082 elif output["status"] != "error":
1083 print_to_stderr("Error: {}".format(output["status_msg"]))
1084
1085 except (KeyError, json.JSONDecodeError):
1086 self._report(
1087 reports.ReportItem.warning(
1088 reports.messages.InvalidResponseFormat(node_label)
1089 )
1090 )
1091
1092 def on_complete(self):
1093 return self._success
1094
1095
1096 def node_add_outside_cluster(
1097 lib: Any, argv: Argv, modifiers: InputModifiers
1098 ) -> None:
1099 """
1100 Options:
1101 * --wait - wait until new node will start up, effective only when --start
1102 is specified
1103 * --start - start new node
1104 * --enable - enable new node
1105 * --force - treat validation issues and not resolvable addresses as
1106 warnings instead of errors
1107 * --skip-offline - skip unreachable nodes
1108 * --no-watchdog-validation - do not validatate watchdogs
1109 * --request-timeout - HTTP request timeout
1110 """
1111 del lib
1112 modifiers.ensure_only_supported(
1113 "--wait",
1114 "--start",
1115 "--enable",
1116 "--force",
1117 "--skip-offline",
1118 "--no-watchdog-validation",
1119 "--request-timeout",
1120 )
1121 if len(argv) < 2:
1122 raise CmdLineInputError(
1123 "Usage: pcs cluster node add-outside <cluster node> <node name> "
1124 "[addr=<node address>]... [watchdog=<watchdog path>] "
1125 "[device=<SBD device path>]... [--start [--wait[=<n>]]] [--enable] "
1126 "[--no-watchdog-validation]"
1127 )
1128
1129 cluster_node, *argv = argv
1130 node_dict = _parse_add_node(argv)
1131
1132 force_flags = []
1133 if modifiers.get("--force"):
1134 force_flags.append(reports.codes.FORCE)
1135 if modifiers.get("--skip-offline"):
1136 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
1137 cmd_data = dict(
1138 nodes=[node_dict],
1139 wait=modifiers.get("--wait"),
1140 start=modifiers.get("--start"),
1141 enable=modifiers.get("--enable"),
1142 no_watchdog_validation=modifiers.get("--no-watchdog-validation"),
1143 force_flags=force_flags,
1144 )
1145
1146 lib_env = utils.get_lib_env()
1147 report_processor = lib_env.report_processor
1148 target_factory = lib_env.get_node_target_factory()
1149 report_list, target_list = target_factory.get_target_list_with_reports(
1150 [cluster_node],
1151 skip_non_existing=False,
1152 allow_skip=False,
1153 )
1154 report_processor.report_list(report_list)
1155 if report_processor.has_errors:
1156 raise LibraryError()
1157
1158 com_cmd = RemoteAddNodes(report_processor, target_list[0], cmd_data)
1159 was_successful = run_com_cmd(lib_env.get_node_communicator(), com_cmd)
1160
1161 if not was_successful:
1162 raise LibraryError()
1163
1164
1165 def node_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1166 """
1167 Options:
1168 * --force - continue even though the action may cause qourum loss
1169 * --skip-offline - skip unreachable nodes
1170 * --request-timeout - HTTP request timeout
1171 """
1172 modifiers.ensure_only_supported(
1173 "--force",
1174 "--skip-offline",
1175 "--request-timeout",
1176 )
1177 if not argv:
1178 raise CmdLineInputError()
1179
1180 force_flags = []
1181 if modifiers.get("--force"):
1182 force_flags.append(reports.codes.FORCE)
1183 if modifiers.get("--skip-offline"):
1184 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
1185
1186 lib.cluster.remove_nodes(argv, force_flags=force_flags)
1187
1188
1189 def cluster_uidgid( # noqa: PLR0912
1190 lib: Any, argv: Argv, modifiers: InputModifiers, silent_list: bool = False
1191 ) -> None:
1192 """
1193 Options: no options
1194 """
1195 del lib
1196 modifiers.ensure_only_supported()
1197 if not argv:
1198 uid_gid_files = os.listdir(settings.corosync_uidgid_dir)
1199 uid_gid_lines: list[str] = []
1200 for ug_file in uid_gid_files:
1201 uid_gid_dict = utils.read_uid_gid_file(ug_file)
1202 if "uid" in uid_gid_dict or "gid" in uid_gid_dict:
1203 line = "UID/GID: uid="
1204 if "uid" in uid_gid_dict:
1205 line += uid_gid_dict["uid"]
1206 line += " gid="
1207 if "gid" in uid_gid_dict:
1208 line += uid_gid_dict["gid"]
1209
1210 uid_gid_lines.append(line)
1211 if uid_gid_lines:
1212 print("\n".join(sorted(uid_gid_lines)))
1213 elif not silent_list:
1214 print_to_stderr("No uidgids configured")
1215 return
1216
1217 command = argv.pop(0)
1218 uid = ""
1219 gid = ""
1220
1221 if command in {"add", "delete", "remove"} and argv:
1222 for arg in argv:
1223 if arg.find("=") == -1:
1224 utils.err(
1225 "uidgid options must be of the form uid=<uid> gid=<gid>"
1226 )
1227
1228 (key, value) = arg.split("=", 1)
1229 if key not in {"uid", "gid"}:
1230 utils.err(
1231 "%s is not a valid key, you must use uid or gid" % key
1232 )
1233
1234 if key == "uid":
1235 uid = value
1236 if key == "gid":
1237 gid = value
1238 if uid == "" and gid == "":
1239 utils.err("you must set either uid or gid")
1240
1241 if command == "add":
1242 utils.write_uid_gid_file(uid, gid)
1243 elif command in {"delete", "remove"}:
1244 file_removed = utils.remove_uid_gid_file(uid, gid)
1245 if not file_removed:
1246 utils.err(
1247 "no uidgid files with uid=%s and gid=%s found" % (uid, gid)
1248 )
1249 else:
1250 raise CmdLineInputError()
1251
1252
1253 # Completely tear down the cluster & remove config files
1254 # Code taken from cluster-clean script in pacemaker
1255 def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
1256 """
1257 Options:
1258 * --all - destroy cluster on all cluster nodes => destroy whole cluster
1259 * --force - required for destroying the cluster - DEPRECATED
1260 * --request-timeout - timeout of HTTP requests, effective only with --all
1261 * --yes - required for destroying the cluster
1262 """
1263 del lib
1264 modifiers.ensure_only_supported(
1265 "--all", "--force", "--request-timeout", "--yes"
1266 )
1267 if argv:
1268 raise CmdLineInputError()
1269 if utils.is_run_interactive():
1270 warn(
1271 "It is recommended to run 'pcs cluster stop' before "
1272 "destroying the cluster."
1273 )
1274 if not utils.get_continue_confirmation(
1275 "This would kill all cluster processes and then PERMANENTLY remove "
1276 "cluster state and configuration",
1277 bool(modifiers.get("--yes")),
1278 bool(modifiers.get("--force")),
1279 ):
1280 return
1281 if modifiers.get("--all"):
1282 # load data
1283 cib = None
1284 lib_env = utils.get_lib_env()
1285 try:
1286 cib = lib_env.get_cib()
1287 except LibraryError:
1288 warn(
1289 "Unable to load CIB to get guest and remote nodes from it, "
1290 "those nodes will not be deconfigured."
1291 )
1292 corosync_nodes, report_list = get_existing_nodes_names(
1293 utils.get_corosync_conf_facade()
1294 )
1295 if not corosync_nodes:
1296 report_list.append(
1297 reports.ReportItem.error(
1298 reports.messages.CorosyncConfigNoNodesDefined()
1299 )
1300 )
1301 if report_list:
1302 process_library_reports(report_list)
1303
1304 # destroy remote and guest nodes
1305 if cib is not None:
1306 try:
1307 all_remote_nodes, report_list = get_existing_nodes_names(
1308 cib=cib
1309 )
1310 if report_list:
1311 process_library_reports(report_list)
1312 if all_remote_nodes:
1313 _destroy_pcmk_remote_env(
1314 lib_env,
1315 all_remote_nodes,
1316 skip_offline_nodes=True,
1317 allow_fails=True,
1318 )
1319 except LibraryError as e:
1320 process_library_reports(list(e.args))
1321
1322 # destroy full-stack nodes
1323 destroy_cluster(corosync_nodes)
1324 else:
1325 print_to_stderr("Shutting down pacemaker/corosync services...")
1326 for service in ["pacemaker", "corosync-qdevice", "corosync"]:
1327 # It is safe to ignore error since we want it not to be running
1328 # anyways.
1329 with contextlib.suppress(LibraryError):
1330 utils.stop_service(service)
1331 print_to_stderr("Killing any remaining services...")
1332 kill_local_cluster_services()
1333 # previously errors were suppressed in here, let's keep it that way
1334 # for now
1335 with contextlib.suppress(Exception):
1336 utils.disableServices()
1337
1338 # it's not a big deal if sbd disable fails
1339 with contextlib.suppress(Exception):
1340 service_manager = utils.get_service_manager()
1341 service_manager.disable(settings.sbd_service_name)
1342
1343 print_to_stderr("Removing all cluster configuration files...")
1344 dummy_output, dummy_retval = utils.run(
1345 [
1346 settings.rm_exec,
1347 "-f",
1348 settings.corosync_conf_file,
1349 settings.corosync_authkey_file,
1350 settings.pacemaker_authkey_file,
1351 settings.pcsd_dr_config_location,
1352 ]
1353 )
1354 state_files = [
1355 "cib-*",
1356 "cib.*",
1357 "cib.xml*",
1358 "core.*",
1359 "cts.*",
1360 "hostcache",
1361 "pe*.bz2",
1362 ]
1363 for name in state_files:
1364 dummy_output, dummy_retval = utils.run(
1365 [
1366 settings.find_exec,
1367 settings.pacemaker_local_state_dir,
1368 "-name",
1369 name,
1370 "-exec",
1371 settings.rm_exec,
1372 "-f",
1373 "{}",
1374 ";",
1375 ]
1376 )
1377 # errors from deleting other files are suppressed as well we do not
1378 # want to fail if qdevice was not set up
1379 with contextlib.suppress(Exception):
1380 qdevice_net.client_destroy()
1381
1382
1383 def cluster_verify(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1384 """
1385 Options:
1386 * -f - CIB file
1387 * --full - more verbose output
1388 """
1389 modifiers.ensure_only_supported("-f", "--full")
1390 if argv:
1391 raise CmdLineInputError()
1392
1393 lib.cluster.verify(verbose=modifiers.get("--full"))
1394
1395
1396 def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
1397 """
1398 Options:
1399 * --force - allow overwriting existing files - DEPRECATED
1400 * --from - timestamp
1401 * --to - timestamp
1402 * --overwrite - allow overwriting existing files
1403 The resulting file should be stored on the machine where pcs cli is
1404 running, not on the machine where pcs daemon is running. Therefore we
1405 want to use --overwrite and not --force.
1406 """
1407
1408 del lib
1409 modifiers.ensure_only_supported("--force", "--from", "--overwrite", "--to")
1410 if len(argv) != 1:
1411 raise CmdLineInputError()
1412
1413 outfile = argv[0]
1414 dest_outfile = outfile + ".tar.bz2"
1415 if os.path.exists(dest_outfile):
1416 if not (modifiers.get("--overwrite") or modifiers.get("--force")):
1417 utils.err(
1418 dest_outfile + " already exists, use --overwrite to overwrite"
1419 )
1420 return
1421 if modifiers.get("--force"):
1422 # deprecated in the first pcs-0.12 version, replaced by --overwrite
1423 deprecation_warning(
1424 "Using --force to confirm this action is deprecated and might "
1425 "be removed in a future release, use --overwrite instead"
1426 )
1427 try:
1428 os.remove(dest_outfile)
1429 except OSError as e:
1430 utils.err(f"Unable to remove {dest_outfile}: {format_os_error(e)}")
1431 crm_report_opts = []
1432
1433 crm_report_opts.append("-f")
1434 if modifiers.is_specified("--from"):
1435 crm_report_opts.append(str(modifiers.get("--from")))
1436 if modifiers.is_specified("--to"):
1437 crm_report_opts.append("-t")
1438 crm_report_opts.append(str(modifiers.get("--to")))
1439 else:
1440 yesterday = datetime.datetime.now() - datetime.timedelta(1)
1441 crm_report_opts.append(yesterday.strftime("%Y-%m-%d %H:%M"))
1442
1443 crm_report_opts.append(outfile)
1444 output, retval = utils.run([settings.crm_report_exec] + crm_report_opts)
1445 if retval != 0 and (
1446 "ERROR: Cannot determine nodes; specify --nodes or --single-node"
1447 in output
1448 ):
1449 utils.err("cluster is not configured on this node")
1450 newoutput = ""
1451 for line in output.split("\n"):
1452 if line.startswith(("cat:", "grep", "tail")):
1453 continue
1454 if "We will attempt to remove" in line:
1455 continue
1456 if "-p option" in line:
1457 continue
1458 if "However, doing" in line:
1459 continue
1460 if "to diagnose" in line:
1461 continue
1462 new_line = line
1463 if "--dest" in line:
1464 new_line = line.replace("--dest", "<dest>")
1465 newoutput = newoutput + new_line + "\n"
1466 if retval != 0:
1467 utils.err(newoutput)
1468 print_to_stderr(newoutput)
1469
1470
1471 # TODO this should be implemented in multiple lib commands, and the cli should
1472 # only call these commands as needed
1473 # - lib command for checking auth, that returns not authorized nodes
1474 # - if any not authorized nodes
1475 # - the cli asks for a username and pass
1476 # - call lib command for authorizing hosts
1477 # - else:
1478 # - call lib command to send the configs to other nodes
1479 #
1480 # This command itself is always run as root, see app.py (_non_root_run)
1481 # So we do not need to deal with the configs in .pcs for non-root run
1482 def cluster_auth_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
1483 """
1484 Options:
1485 * --corosync_conf - corosync.conf file
1486 * --request-timeout - timeout of HTTP requests
1487 * -u - username
1488 * -p - password
1489 """
1490 modifiers.ensure_only_supported(
1491 "--corosync_conf", "--request-timeout", "-u", "-p"
1492 )
1493 if argv:
1494 raise CmdLineInputError()
1495 lib_env = utils.get_lib_env()
1496 target_factory = lib_env.get_node_target_factory()
1497 corosync_conf = lib_env.get_corosync_conf()
1498 cluster_node_list = corosync_conf.get_nodes()
1499 cluster_node_names = []
1500 missing_name = False
1501 for node in cluster_node_list:
1502 if node.name:
1503 cluster_node_names.append(node.name)
1504 else:
1505 missing_name = True
1506 if missing_name:
1507 warn(
1508 "Skipping nodes which do not have their name defined in "
1509 "corosync.conf, use the 'pcs host auth' command to authenticate "
1510 "them"
1511 )
1512 target_list = []
1513 not_authorized_node_name_list = []
1514 for node_name in cluster_node_names:
1515 try:
1516 target_list.append(target_factory.get_target(node_name))
1517 except HostNotFound:
1518 print_to_stderr("{}: Not authorized".format(node_name))
1519 not_authorized_node_name_list.append(node_name)
1520 com_cmd = CheckAuth(lib_env.report_processor)
1521 com_cmd.set_targets(target_list)
1522 not_authorized_node_name_list.extend(
1523 run_and_raise(lib_env.get_node_communicator(), com_cmd)
1524 )
1525 if not_authorized_node_name_list:
1526 print(
1527 "Nodes to authorize: {}".format(
1528 ", ".join(not_authorized_node_name_list)
1529 )
1530 )
1531 username, password = utils.get_user_and_pass()
1532 not_auth_node_list = []
1533 for node_name in not_authorized_node_name_list:
1534 for node in cluster_node_list:
1535 if node.name == node_name:
1536 if node.addrs_plain():
1537 not_auth_node_list.append(node)
1538 else:
1539 print_to_stderr(
1540 f"{node.name}: No addresses defined in "
1541 "corosync.conf, use the 'pcs host auth' command to "
1542 "authenticate the node"
1543 )
1544 nodes_to_auth_data = {
1545 node.name: HostAuthData(
1546 username,
1547 password,
1548 [
1549 Destination(
1550 node.addrs_plain()[0], settings.pcsd_default_port
1551 )
1552 ],
1553 )
1554 for node in not_auth_node_list
1555 }
1556 lib.auth.auth_hosts(nodes_to_auth_data)
1557 else:
1558 # TODO backwards compatibility
1559 # The command overwrites known-hosts and pcsd_settings.conf on all
1560 # cluster nodes with local version, only if all of the nodes are
1561 # already authorized. We should investigate what is the reason why
1562 # the command does this, and decide if we should drop/keep/change this
1563 configs = {}
1564 for file_type_code in SYNCED_CONFIGS:
1565 file_instance = FileInstance.for_common(file_type_code)
1566 if not file_instance.raw_file.exists():
1567 # it's not an error if the file does not exist locally, we just
1568 # wont send it
1569 continue
1570 try:
1571 configs[file_type_code] = file_instance.read_raw().decode(
1572 "utf-8"
1573 )
1574 except RawFileError as e:
1575 # in case of error when reading some file, we still might be able
1576 # to read and send the others without issues
1577 lib_env.report_processor.report(
1578 raw_file_error_report(e, is_forced_or_warning=True)
1579 )
1580 set_configs_cmd = SetConfigs(
1581 lib_env.report_processor,
1582 corosync_conf.get_cluster_name(),
1583 configs,
1584 force=True,
1585 rejection_severity=reports.ReportItemSeverity.error(),
1586 )
1587 set_configs_cmd.set_targets(target_list)
1588 run_and_raise(lib_env.get_node_communicator(), set_configs_cmd)
1589
1590
1591 def _parse_node_options(
1592 node: str,
1593 options: Argv,
1594 additional_options: StringCollection = (),
1595 additional_repeatable_options: StringCollection = (),
1596 ) -> dict[str, str | list[str]]:
1597 """
1598 Commandline options: no options
1599 """
1600 ADDR_OPT_KEYWORD = "addr"
1601 supported_options = {ADDR_OPT_KEYWORD} | set(additional_options)
1602 repeatable_options = {ADDR_OPT_KEYWORD} | set(additional_repeatable_options)
1603 parser = KeyValueParser(options, repeatable_options)
1604 parsed_unique = parser.get_unique()
1605 parsed_repeatable = parser.get_repeatable()
1606 unknown_options = (
1607 set(parsed_unique.keys()) | set(parsed_repeatable)
1608 ) - supported_options
1609 if unknown_options:
1610 raise CmdLineInputError(
1611 f"Unknown options {format_list(unknown_options)} for node '{node}'"
1612 )
1613 parsed_unique["name"] = node
1614 if ADDR_OPT_KEYWORD in parsed_repeatable:
1615 parsed_repeatable["addrs"] = parsed_repeatable[ADDR_OPT_KEYWORD]
1616 del parsed_repeatable[ADDR_OPT_KEYWORD]
1617 return parsed_unique | parsed_repeatable
1618
1619
1620 TRANSPORT_KEYWORD = "transport"
1621 TRANSPORT_DEFAULT_SECTION = "__default__"
1622 LINK_KEYWORD = "link"
1623
1624
1625 def _parse_transport(
1626 transport_args: Argv,
1627 ) -> tuple[str, dict[str, dict[str, str] | list[dict[str, str]]]]:
1628 """
1629 Commandline options: no options
1630 """
1631 if not transport_args:
1632 raise CmdLineInputError(
1633 f"{TRANSPORT_KEYWORD.capitalize()} type not defined"
1634 )
1635 transport_type, *transport_options = transport_args
1636
1637 keywords = {"compression", "crypto", LINK_KEYWORD}
1638 parsed_options = parse_args.group_by_keywords(
1639 transport_options,
1640 keywords,
1641 implicit_first_keyword=TRANSPORT_DEFAULT_SECTION,
1642 )
1643 options: dict[str, dict[str, str] | list[dict[str, str]]] = {
1644 section: KeyValueParser(
1645 parsed_options.get_args_flat(section)
1646 ).get_unique()
1647 for section in keywords | {TRANSPORT_DEFAULT_SECTION}
1648 if section != LINK_KEYWORD
1649 }
1650 options[LINK_KEYWORD] = [
1651 KeyValueParser(link_options).get_unique()
1652 for link_options in parsed_options.get_args_groups(LINK_KEYWORD)
1653 ]
1654
1655 return transport_type, options
1656
1657
1658 def cluster_setup(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1659 """
1660 Options:
1661 * --wait - only effective when used with --start
1662 * --start - start cluster
1663 * --enable - enable cluster
1664 * --force - some validation issues and unresolvable addresses are treated
1665 as warnings
1666 * --no-keys-sync - do not create and distribute pcsd ssl cert and key,
1667 corosync and pacemaker authkeys
1668 * --no-cluster-uuid - do not generate a cluster UUID during setup
1669 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
1670 * --overwrite - allow overwriting existing files
1671 """
1672 is_local = modifiers.is_specified("--corosync_conf")
1673
1674 allowed_options_common = ["--force", "--no-cluster-uuid"]
1675 allowed_options_live = [
1676 "--wait",
1677 "--start",
1678 "--enable",
1679 "--no-keys-sync",
1680 ]
1681 allowed_options_local = ["--corosync_conf", "--overwrite"]
1682 modifiers.ensure_only_supported(
1683 *(
1684 allowed_options_common
1685 + allowed_options_live
1686 + allowed_options_local
1687 ),
1688 )
1689 if is_local and modifiers.is_specified_any(allowed_options_live):
1690 raise CmdLineInputError(
1691 f"Cannot specify any of {format_list(allowed_options_live)} "
1692 "when '--corosync_conf' is specified"
1693 )
1694 if not is_local and modifiers.is_specified("--overwrite"):
1695 raise CmdLineInputError(
1696 "Cannot specify '--overwrite' when '--corosync_conf' is not "
1697 "specified"
1698 )
1699
1700 if len(argv) < 2:
1701 raise CmdLineInputError()
1702 cluster_name, *argv = argv
1703 keywords = [TRANSPORT_KEYWORD, "totem", "quorum"]
1704 parsed_args = parse_args.group_by_keywords(
1705 argv, keywords, implicit_first_keyword="nodes"
1706 )
1707 parsed_args.ensure_unique_keywords()
1708 nodes = [
1709 _parse_node_options(node, options)
1710 for node, options in parse_args.split_list_by_any_keywords(
1711 parsed_args.get_args_flat("nodes"), "node name"
1712 ).items()
1713 ]
1714
1715 transport_type = None
1716 transport_options: dict[str, dict[str, str] | list[dict[str, str]]] = {}
1717
1718 if parsed_args.has_keyword(TRANSPORT_KEYWORD):
1719 transport_type, transport_options = _parse_transport(
1720 parsed_args.get_args_flat(TRANSPORT_KEYWORD)
1721 )
1722
1723 force_flags = []
1724 if modifiers.get("--force"):
1725 force_flags.append(reports.codes.FORCE)
1726
1727 totem_options = KeyValueParser(
1728 parsed_args.get_args_flat("totem")
1729 ).get_unique()
1730 quorum_options = KeyValueParser(
1731 parsed_args.get_args_flat("quorum")
1732 ).get_unique()
1733
1734 if not is_local:
1735 lib.cluster.setup(
1736 cluster_name,
1737 nodes,
1738 transport_type=transport_type,
1739 transport_options=transport_options.get(
1740 TRANSPORT_DEFAULT_SECTION, {}
1741 ),
1742 link_list=transport_options.get(LINK_KEYWORD, []),
1743 compression_options=transport_options.get("compression", {}),
1744 crypto_options=transport_options.get("crypto", {}),
1745 totem_options=totem_options,
1746 quorum_options=quorum_options,
1747 wait=modifiers.get("--wait"),
1748 start=modifiers.get("--start"),
1749 enable=modifiers.get("--enable"),
1750 no_keys_sync=modifiers.get("--no-keys-sync"),
1751 no_cluster_uuid=modifiers.is_specified("--no-cluster-uuid"),
1752 force_flags=force_flags,
1753 )
1754 return
1755
1756 corosync_conf_data = lib.cluster.setup_local(
1757 cluster_name,
1758 nodes,
1759 transport_type=transport_type,
1760 transport_options=transport_options.get(TRANSPORT_DEFAULT_SECTION, {}),
1761 link_list=transport_options.get(LINK_KEYWORD, []),
1762 compression_options=transport_options.get("compression", {}),
1763 crypto_options=transport_options.get("crypto", {}),
1764 totem_options=totem_options,
1765 quorum_options=quorum_options,
1766 no_cluster_uuid=modifiers.is_specified("--no-cluster-uuid"),
1767 force_flags=force_flags,
1768 )
1769
1770 corosync_conf_file = pcs_file.RawFile(
1771 file_metadata.for_file_type(
1772 file_type_codes.COROSYNC_CONF, modifiers.get("--corosync_conf")
1773 )
1774 )
1775 overwrite = modifiers.is_specified("--overwrite")
1776 try:
1777 corosync_conf_file.write(corosync_conf_data, can_overwrite=overwrite)
1778 except pcs_file.FileAlreadyExists as e:
1779 utils.err(
1780 reports.messages.FileAlreadyExists(
1781 e.metadata.file_type_code,
1782 e.metadata.path,
1783 ).message
1784 + ", use --overwrite to overwrite existing file(s)"
1785 )
1786 except pcs_file.RawFileError as e:
1787 utils.err(
1788 reports.messages.FileIoError(
1789 e.metadata.file_type_code,
1790 e.action,
1791 e.reason,
1792 file_path=e.metadata.path,
1793 ).message
1794 )
1795
1796
1797 def config_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1798 """
1799 Options:
1800 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
1801 """
1802 modifiers.ensure_only_supported("--corosync_conf")
1803 parsed_args = parse_args.group_by_keywords(
1804 argv,
1805 ["transport", "compression", "crypto", "totem"],
1806 )
1807
1808 transport_options = KeyValueParser(
1809 parsed_args.get_args_flat("transport")
1810 ).get_unique()
1811 compression_options = KeyValueParser(
1812 parsed_args.get_args_flat("compression")
1813 ).get_unique()
1814 crypto_options = KeyValueParser(
1815 parsed_args.get_args_flat("crypto")
1816 ).get_unique()
1817 totem_options = KeyValueParser(
1818 parsed_args.get_args_flat("totem")
1819 ).get_unique()
1820
1821 if not modifiers.is_specified("--corosync_conf"):
1822 lib.cluster.config_update(
1823 transport_options,
1824 compression_options,
1825 crypto_options,
1826 totem_options,
1827 )
1828 return
1829
1830 _corosync_conf_local_cmd_call(
1831 modifiers.get("--corosync_conf"),
1832 lambda corosync_conf_content: lib.cluster.config_update_local(
1833 corosync_conf_content,
1834 transport_options,
1835 compression_options,
1836 crypto_options,
1837 totem_options,
1838 ),
1839 )
1840
1841
1842 def _format_options(label: str, options: Mapping[str, str]) -> list[str]:
1843 output = []
1844 if options:
1845 output.append(f"{label}:")
1846 output.extend(
1847 indent([f"{opt}: {val}" for opt, val in sorted(options.items())])
1848 )
1849 return output
1850
1851
1852 def _format_nodes(nodes: Iterable[CorosyncNodeDto]) -> list[str]:
1853 output = ["Nodes:"]
1854 for node in sorted(nodes, key=lambda node: node.name):
1855 node_attrs = [
1856 f"Link {addr.link} address: {addr.addr}"
1857 for addr in sorted(node.addrs, key=lambda addr: addr.link)
1858 ] + [f"nodeid: {node.nodeid}"]
1859 output.extend(indent([f"{node.name}:"] + indent(node_attrs)))
1860 return output
1861
1862
1863 def config_show(
1864 lib: Any, argv: Argv, modifiers: parse_args.InputModifiers
1865 ) -> None:
1866 """
1867 Options:
1868 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
1869 * --output-format - supported formats: text, cmd, json
1870 """
1871 modifiers.ensure_only_supported(
1872 "--corosync_conf", output_format_supported=True
1873 )
1874 if argv:
1875 raise CmdLineInputError()
1876 output_format = modifiers.get_output_format()
1877 corosync_conf_dto = lib.cluster.get_corosync_conf_struct()
1878 if output_format == OUTPUT_FORMAT_VALUE_CMD:
1879 if corosync_conf_dto.quorum_device is not None:
1880 warn(
1881 "Quorum device configuration detected but not yet supported by "
1882 "this command."
1883 )
1884 output = " \\\n".join(_config_get_cmd(corosync_conf_dto))
1885 elif output_format == OUTPUT_FORMAT_VALUE_JSON:
1886 output = json.dumps(dto.to_dict(corosync_conf_dto))
1887 else:
1888 output = "\n".join(_config_get_text(corosync_conf_dto))
1889 print(output)
1890
1891
1892 def _config_get_text(corosync_conf: CorosyncConfDto) -> list[str]:
1893 lines = [f"Cluster Name: {corosync_conf.cluster_name}"]
1894 if corosync_conf.cluster_uuid:
1895 lines.append(f"Cluster UUID: {corosync_conf.cluster_uuid}")
1896 lines.append(f"Transport: {corosync_conf.transport.lower()}")
1897 lines.extend(_format_nodes(corosync_conf.nodes))
1898 if corosync_conf.links_options:
1899 lines.append("Links:")
1900 for linknum, link_options in sorted(
1901 corosync_conf.links_options.items()
1902 ):
1903 lines.extend(
1904 indent(_format_options(f"Link {linknum}", link_options))
1905 )
1906
1907 lines.extend(
1908 _format_options("Transport Options", corosync_conf.transport_options)
1909 )
1910 lines.extend(
1911 _format_options(
1912 "Compression Options", corosync_conf.compression_options
1913 )
1914 )
1915 lines.extend(
1916 _format_options("Crypto Options", corosync_conf.crypto_options)
1917 )
1918 lines.extend(_format_options("Totem Options", corosync_conf.totem_options))
1919 lines.extend(
1920 _format_options("Quorum Options", corosync_conf.quorum_options)
1921 )
1922 if corosync_conf.quorum_device:
1923 lines.append(f"Quorum Device: {corosync_conf.quorum_device.model}")
1924 lines.extend(
1925 indent(
1926 _format_options(
1927 "Options", corosync_conf.quorum_device.generic_options
1928 )
1929 )
1930 )
1931 lines.extend(
1932 indent(
1933 _format_options(
1934 "Model Options",
1935 corosync_conf.quorum_device.model_options,
1936 )
1937 )
1938 )
1939 lines.extend(
1940 indent(
1941 _format_options(
1942 "Heuristics",
1943 corosync_conf.quorum_device.heuristics_options,
1944 )
1945 )
1946 )
1947 return lines
1948
1949
1950 def _corosync_node_to_cmd_line(node: CorosyncNodeDto) -> str:
1951 return " ".join(
1952 [node.name]
1953 + [
1954 f"addr={addr.addr}"
1955 for addr in sorted(node.addrs, key=lambda addr: addr.link)
1956 ]
1957 )
1958
1959
1960 def _section_to_lines(
1961 options: Mapping[str, str], keyword: str | None = None
1962 ) -> list[str]:
1963 output: list[str] = []
1964 if options:
1965 if keyword:
1966 output.append(keyword)
1967 output.extend(
1968 indent([f"{key}={val}" for key, val in sorted(options.items())])
1969 )
1970 return indent(output)
1971
1972
1973 def _config_get_cmd(corosync_conf: CorosyncConfDto) -> list[str]:
1974 lines = [f"pcs cluster setup {corosync_conf.cluster_name}"]
1975 lines += indent(
1976 [
1977 _corosync_node_to_cmd_line(node)
1978 for node in sorted(
1979 corosync_conf.nodes, key=lambda node: node.nodeid
1980 )
1981 ]
1982 )
1983 transport = [
1984 "transport",
1985 str(corosync_conf.transport.value).lower(),
1986 ] + _section_to_lines(corosync_conf.transport_options)
1987 for _, link in sorted(corosync_conf.links_options.items()):
1988 transport.extend(_section_to_lines(link, "link"))
1989 transport.extend(
1990 _section_to_lines(corosync_conf.compression_options, "compression")
1991 )
1992 transport.extend(_section_to_lines(corosync_conf.crypto_options, "crypto"))
1993 lines.extend(indent(transport))
1994 lines.extend(_section_to_lines(corosync_conf.totem_options, "totem"))
1995 lines.extend(_section_to_lines(corosync_conf.quorum_options, "quorum"))
1996 if not corosync_conf.cluster_uuid:
1997 lines.extend(indent(["--no-cluster-uuid"]))
1998 return lines
1999
2000
2001 def _parse_add_node(argv: Argv) -> dict[str, str | list[str]]:
2002 DEVICE_KEYWORD = "device"
2003 WATCHDOG_KEYWORD = "watchdog"
2004 hostname, *argv = argv
2005 node_dict = _parse_node_options(
2006 hostname,
2007 argv,
2008 additional_options={DEVICE_KEYWORD, WATCHDOG_KEYWORD},
2009 additional_repeatable_options={DEVICE_KEYWORD},
2010 )
2011 if DEVICE_KEYWORD in node_dict:
2012 node_dict[f"{DEVICE_KEYWORD}s"] = node_dict[DEVICE_KEYWORD]
2013 del node_dict[DEVICE_KEYWORD]
2014 return node_dict
2015
2016
2017 def node_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2018 """
2019 Options:
2020 * --wait - wait until new node will start up, effective only when --start
2021 is specified
2022 * --start - start new node
2023 * --enable - enable new node
2024 * --force - treat validation issues and not resolvable addresses as
2025 warnings instead of errors
2026 * --skip-offline - skip unreachable nodes
2027 * --no-watchdog-validation - do not validatate watchdogs
2028 * --request-timeout - HTTP request timeout
2029 """
2030 modifiers.ensure_only_supported(
2031 "--wait",
2032 "--start",
2033 "--enable",
2034 "--force",
2035 "--skip-offline",
2036 "--no-watchdog-validation",
2037 "--request-timeout",
2038 )
2039 if not argv:
2040 raise CmdLineInputError()
2041
2042 node_dict = _parse_add_node(argv)
2043
2044 force_flags = []
2045 if modifiers.get("--force"):
2046 force_flags.append(reports.codes.FORCE)
2047 if modifiers.get("--skip-offline"):
2048 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2049
2050 lib.cluster.add_nodes(
2051 nodes=[node_dict],
2052 wait=modifiers.get("--wait"),
2053 start=modifiers.get("--start"),
2054 enable=modifiers.get("--enable"),
2055 no_watchdog_validation=modifiers.get("--no-watchdog-validation"),
2056 force_flags=force_flags,
2057 )
2058
2059
2060 def remove_nodes_from_cib(
2061 lib: Any, argv: Argv, modifiers: InputModifiers
2062 ) -> None:
2063 """
2064 Options: no options
2065 """
2066 modifiers.ensure_only_supported()
2067 if not argv:
2068 raise CmdLineInputError("No nodes specified")
2069 lib.cluster.remove_nodes_from_cib(argv)
2070
2071
2072 def link_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2073 """
2074 Options:
2075 * --force - treat validation issues and not resolvable addresses as
2076 warnings instead of errors
2077 * --skip-offline - skip unreachable nodes
2078 * --request-timeout - HTTP request timeout
2079 """
2080 modifiers.ensure_only_supported(
2081 "--force", "--request-timeout", "--skip-offline"
2082 )
2083 if not argv:
2084 raise CmdLineInputError()
2085
2086 force_flags = []
2087 if modifiers.get("--force"):
2088 force_flags.append(reports.codes.FORCE)
2089 if modifiers.get("--skip-offline"):
2090 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2091
2092 parsed = parse_args.group_by_keywords(
2093 argv, {"options"}, implicit_first_keyword="nodes"
2094 )
2095 parsed.ensure_unique_keywords()
2096
2097 lib.cluster.add_link(
2098 KeyValueParser(parsed.get_args_flat("nodes")).get_unique(),
2099 KeyValueParser(parsed.get_args_flat("options")).get_unique(),
2100 force_flags=force_flags,
2101 )
2102
2103
2104 def link_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2105 """
2106 Options:
2107 * --skip-offline - skip unreachable nodes
2108 * --request-timeout - HTTP request timeout
2109 """
2110 modifiers.ensure_only_supported("--request-timeout", "--skip-offline")
2111 if not argv:
2112 raise CmdLineInputError()
2113
2114 force_flags = []
2115 if modifiers.get("--skip-offline"):
2116 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2117
2118 lib.cluster.remove_links(argv, force_flags=force_flags)
2119
2120
2121 def link_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2122 """
2123 Options:
2124 * --force - treat validation issues and not resolvable addresses as
2125 warnings instead of errors
2126 * --skip-offline - skip unreachable nodes
2127 * --request-timeout - HTTP request timeout
2128 """
2129 modifiers.ensure_only_supported(
2130 "--force", "--request-timeout", "--skip-offline"
2131 )
2132 if len(argv) < 2:
2133 raise CmdLineInputError()
2134
2135 force_flags = []
2136 if modifiers.get("--force"):
2137 force_flags.append(reports.codes.FORCE)
2138 if modifiers.get("--skip-offline"):
2139 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2140
2141 linknumber = argv[0]
2142 parsed = parse_args.group_by_keywords(
2143 argv[1:], {"options"}, implicit_first_keyword="nodes"
2144 )
2145 parsed.ensure_unique_keywords()
2146
2147 lib.cluster.update_link(
2148 linknumber,
2149 KeyValueParser(parsed.get_args_flat("nodes")).get_unique(),
2150 KeyValueParser(parsed.get_args_flat("options")).get_unique(),
2151 force_flags=force_flags,
2152 )
2153
2154
2155 def generate_uuid(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2156 """
2157 Options:
2158 * --force - allow to rewrite an existing UUID in corosync.conf
2159 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
2160 """
2161 modifiers.ensure_only_supported("--force", "--corosync_conf")
2162 if argv:
2163 raise CmdLineInputError()
2164
2165 force_flags = []
2166 if modifiers.get("--force"):
2167 force_flags.append(reports.codes.FORCE)
2168
2169 if not modifiers.is_specified("--corosync_conf"):
2170 lib.cluster.generate_cluster_uuid(force_flags=force_flags)
2171 return
2172
2173 _corosync_conf_local_cmd_call(
2174 modifiers.get("--corosync_conf"),
2175 lambda corosync_conf_content: lib.cluster.generate_cluster_uuid_local(
2176 corosync_conf_content, force_flags=force_flags
2177 ),
2178 )
2179