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 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 * --request-timeout - timeout of HTTP requests, effective only with --all
1260 * --yes - required for destroying the cluster
1261 """
1262 del lib
1263 modifiers.ensure_only_supported(
1264 "--all",
1265 "--request-timeout",
1266 "--yes",
1267 hint_syntax_changed="1.0",
1268 )
1269 if argv:
1270 raise CmdLineInputError()
1271 if utils.is_run_interactive():
1272 warn(
1273 "It is recommended to run 'pcs cluster stop' before "
1274 "destroying the cluster."
1275 )
1276 if not utils.get_continue_confirmation(
1277 "This would kill all cluster processes and then PERMANENTLY remove "
1278 "cluster state and configuration",
1279 bool(modifiers.get("--yes")),
1280 ):
1281 return
1282 if modifiers.get("--all"):
1283 # load data
1284 cib = None
1285 lib_env = utils.get_lib_env()
1286 try:
1287 cib = lib_env.get_cib()
1288 except LibraryError:
1289 warn(
1290 "Unable to load CIB to get guest and remote nodes from it, "
1291 "those nodes will not be deconfigured."
1292 )
1293 corosync_nodes, report_list = get_existing_nodes_names(
1294 utils.get_corosync_conf_facade()
1295 )
1296 if not corosync_nodes:
1297 report_list.append(
1298 reports.ReportItem.error(
1299 reports.messages.CorosyncConfigNoNodesDefined()
1300 )
1301 )
1302 if report_list:
1303 process_library_reports(report_list)
1304
1305 # destroy remote and guest nodes
1306 if cib is not None:
1307 try:
1308 all_remote_nodes, report_list = get_existing_nodes_names(
1309 cib=cib
1310 )
1311 if report_list:
1312 process_library_reports(report_list)
1313 if all_remote_nodes:
1314 _destroy_pcmk_remote_env(
1315 lib_env,
1316 all_remote_nodes,
1317 skip_offline_nodes=True,
1318 allow_fails=True,
1319 )
1320 except LibraryError as e:
1321 process_library_reports(list(e.args))
1322
1323 # destroy full-stack nodes
1324 destroy_cluster(corosync_nodes)
1325 else:
1326 print_to_stderr("Shutting down pacemaker/corosync services...")
1327 for service in ["pacemaker", "corosync-qdevice", "corosync"]:
1328 # It is safe to ignore error since we want it not to be running
1329 # anyways.
1330 with contextlib.suppress(LibraryError):
1331 utils.stop_service(service)
1332 print_to_stderr("Killing any remaining services...")
1333 kill_local_cluster_services()
1334 # previously errors were suppressed in here, let's keep it that way
1335 # for now
1336 with contextlib.suppress(Exception):
1337 utils.disableServices()
1338
1339 # it's not a big deal if sbd disable fails
1340 with contextlib.suppress(Exception):
1341 service_manager = utils.get_service_manager()
1342 service_manager.disable(settings.sbd_service_name)
1343
1344 print_to_stderr("Removing all cluster configuration files...")
1345 dummy_output, dummy_retval = utils.run(
1346 [
1347 settings.rm_exec,
1348 "-f",
1349 settings.corosync_conf_file,
1350 settings.corosync_authkey_file,
1351 settings.pacemaker_authkey_file,
1352 settings.pcsd_dr_config_location,
1353 ]
1354 )
1355 state_files = [
1356 "cib-*",
1357 "cib.*",
1358 "cib.xml*",
1359 "core.*",
1360 "cts.*",
1361 "hostcache",
1362 "pe*.bz2",
1363 ]
1364 for name in state_files:
1365 dummy_output, dummy_retval = utils.run(
1366 [
1367 settings.find_exec,
1368 settings.pacemaker_local_state_dir,
1369 "-name",
1370 name,
1371 "-exec",
1372 settings.rm_exec,
1373 "-f",
1374 "{}",
1375 ";",
1376 ]
1377 )
1378 # errors from deleting other files are suppressed as well we do not
1379 # want to fail if qdevice was not set up
1380 with contextlib.suppress(Exception):
1381 qdevice_net.client_destroy()
1382
1383
1384 def cluster_verify(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1385 """
1386 Options:
1387 * -f - CIB file
1388 * --full - more verbose output
1389 """
1390 modifiers.ensure_only_supported("-f", "--full")
1391 if argv:
1392 raise CmdLineInputError()
1393
1394 lib.cluster.verify(verbose=modifiers.get("--full"))
1395
1396
1397 def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
1398 """
1399 Options:
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(
1410 "--from",
1411 "--overwrite",
1412 "--to",
1413 hint_syntax_changed="1.0",
1414 )
1415 if len(argv) != 1:
1416 raise CmdLineInputError()
1417
1418 outfile = argv[0]
1419 dest_outfile = outfile + ".tar.bz2"
1420 if os.path.exists(dest_outfile):
1421 if not modifiers.get("--overwrite"):
1422 utils.err(
1423 dest_outfile + " already exists, use --overwrite to overwrite"
1424 )
1425 return
1426 try:
1427 os.remove(dest_outfile)
1428 except OSError as e:
1429 utils.err(f"Unable to remove {dest_outfile}: {format_os_error(e)}")
1430 crm_report_opts = []
1431
1432 crm_report_opts.append("-f")
1433 if modifiers.is_specified("--from"):
1434 crm_report_opts.append(str(modifiers.get("--from")))
1435 if modifiers.is_specified("--to"):
1436 crm_report_opts.append("-t")
1437 crm_report_opts.append(str(modifiers.get("--to")))
1438 else:
1439 yesterday = datetime.datetime.now() - datetime.timedelta(1)
1440 crm_report_opts.append(yesterday.strftime("%Y-%m-%d %H:%M"))
1441
1442 crm_report_opts.append(outfile)
1443 output, retval = utils.run([settings.crm_report_exec] + crm_report_opts)
1444 if retval != 0 and (
1445 "ERROR: Cannot determine nodes; specify --nodes or --single-node"
1446 in output
1447 ):
1448 utils.err("cluster is not configured on this node")
1449 newoutput = ""
1450 for line in output.split("\n"):
1451 if line.startswith(("cat:", "grep", "tail")):
1452 continue
1453 if "We will attempt to remove" in line:
1454 continue
1455 if "-p option" in line:
1456 continue
1457 if "However, doing" in line:
1458 continue
1459 if "to diagnose" in line:
1460 continue
1461 new_line = line
1462 if "--dest" in line:
1463 new_line = line.replace("--dest", "<dest>")
1464 newoutput = newoutput + new_line + "\n"
1465 if retval != 0:
1466 utils.err(newoutput)
1467 print_to_stderr(newoutput)
1468
1469
1470 # TODO this should be implemented in multiple lib commands, and the cli should
1471 # only call these commands as needed
1472 # - lib command for checking auth, that returns not authorized nodes
1473 # - if any not authorized nodes
1474 # - the cli asks for a username and pass
1475 # - call lib command for authorizing hosts
1476 # - else:
1477 # - call lib command to send the configs to other nodes
1478 #
1479 # This command itself is always run as root, see app.py (_non_root_run)
1480 # So we do not need to deal with the configs in .pcs for non-root run
1481 def cluster_auth_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: # noqa: PLR0912
1482 """
1483 Options:
1484 * --corosync_conf - corosync.conf file
1485 * --request-timeout - timeout of HTTP requests
1486 * -u - username
1487 * -p - password
1488 """
1489 modifiers.ensure_only_supported(
1490 "--corosync_conf", "--request-timeout", "-u", "-p"
1491 )
1492 if argv:
1493 raise CmdLineInputError()
1494 lib_env = utils.get_lib_env()
1495 target_factory = lib_env.get_node_target_factory()
1496 corosync_conf = lib_env.get_corosync_conf()
1497 cluster_node_list = corosync_conf.get_nodes()
1498 cluster_node_names = []
1499 missing_name = False
1500 for node in cluster_node_list:
1501 if node.name:
1502 cluster_node_names.append(node.name)
1503 else:
1504 missing_name = True
1505 if missing_name:
1506 warn(
1507 "Skipping nodes which do not have their name defined in "
1508 "corosync.conf, use the 'pcs host auth' command to authenticate "
1509 "them"
1510 )
1511 target_list = []
1512 not_authorized_node_name_list = []
1513 for node_name in cluster_node_names:
1514 try:
1515 target_list.append(target_factory.get_target(node_name))
1516 except HostNotFound:
1517 print_to_stderr("{}: Not authorized".format(node_name))
1518 not_authorized_node_name_list.append(node_name)
1519 com_cmd = CheckAuth(lib_env.report_processor)
1520 com_cmd.set_targets(target_list)
1521 not_authorized_node_name_list.extend(
1522 run_and_raise(lib_env.get_node_communicator(), com_cmd)
1523 )
1524 if not_authorized_node_name_list:
1525 print(
1526 "Nodes to authorize: {}".format(
1527 ", ".join(not_authorized_node_name_list)
1528 )
1529 )
1530 username, password = utils.get_user_and_pass()
1531 not_auth_node_list = []
1532 for node_name in not_authorized_node_name_list:
1533 for node in cluster_node_list:
1534 if node.name == node_name:
1535 if node.addrs_plain():
1536 not_auth_node_list.append(node)
1537 else:
1538 print_to_stderr(
1539 f"{node.name}: No addresses defined in "
1540 "corosync.conf, use the 'pcs host auth' command to "
1541 "authenticate the node"
1542 )
1543 nodes_to_auth_data = {
1544 node.name: HostAuthData(
1545 username,
1546 password,
1547 [
1548 Destination(
1549 node.addrs_plain()[0], settings.pcsd_default_port
1550 )
1551 ],
1552 )
1553 for node in not_auth_node_list
1554 }
1555 lib.auth.auth_hosts(nodes_to_auth_data)
1556 else:
1557 # TODO backwards compatibility
1558 # The command overwrites known-hosts and pcsd_settings.conf on all
1559 # cluster nodes with local version, only if all of the nodes are
1560 # already authorized. We should investigate what is the reason why
1561 # the command does this, and decide if we should drop/keep/change this
1562 configs = {}
1563 for file_type_code in SYNCED_CONFIGS:
1564 file_instance = FileInstance.for_common(file_type_code)
1565 if not file_instance.raw_file.exists():
1566 # it's not an error if the file does not exist locally, we just
1567 # wont send it
1568 continue
1569 try:
1570 configs[file_type_code] = file_instance.read_raw().decode(
1571 "utf-8"
1572 )
1573 except RawFileError as e:
1574 # in case of error when reading some file, we still might be able
1575 # to read and send the others without issues
1576 lib_env.report_processor.report(
1577 raw_file_error_report(e, is_forced_or_warning=True)
1578 )
1579 set_configs_cmd = SetConfigs(
1580 lib_env.report_processor,
1581 corosync_conf.get_cluster_name(),
1582 configs,
1583 force=True,
1584 rejection_severity=reports.ReportItemSeverity.error(),
1585 )
1586 set_configs_cmd.set_targets(target_list)
1587 run_and_raise(lib_env.get_node_communicator(), set_configs_cmd)
1588
1589
1590 def _parse_node_options(
1591 node: str,
1592 options: Argv,
1593 additional_options: StringCollection = (),
1594 additional_repeatable_options: StringCollection = (),
1595 ) -> dict[str, str | list[str]]:
1596 """
1597 Commandline options: no options
1598 """
1599 ADDR_OPT_KEYWORD = "addr"
1600 supported_options = {ADDR_OPT_KEYWORD} | set(additional_options)
1601 repeatable_options = {ADDR_OPT_KEYWORD} | set(additional_repeatable_options)
1602 parser = KeyValueParser(options, repeatable_options)
1603 parsed_unique = parser.get_unique()
1604 parsed_repeatable = parser.get_repeatable()
1605 unknown_options = (
1606 set(parsed_unique.keys()) | set(parsed_repeatable)
1607 ) - supported_options
1608 if unknown_options:
1609 raise CmdLineInputError(
1610 f"Unknown options {format_list(unknown_options)} for node '{node}'"
1611 )
1612 parsed_unique["name"] = node
1613 if ADDR_OPT_KEYWORD in parsed_repeatable:
1614 parsed_repeatable["addrs"] = parsed_repeatable[ADDR_OPT_KEYWORD]
1615 del parsed_repeatable[ADDR_OPT_KEYWORD]
1616 return parsed_unique | parsed_repeatable
1617
1618
1619 TRANSPORT_KEYWORD = "transport"
1620 TRANSPORT_DEFAULT_SECTION = "__default__"
1621 LINK_KEYWORD = "link"
1622
1623
1624 def _parse_transport(
1625 transport_args: Argv,
1626 ) -> tuple[str, dict[str, dict[str, str] | list[dict[str, str]]]]:
1627 """
1628 Commandline options: no options
1629 """
1630 if not transport_args:
1631 raise CmdLineInputError(
1632 f"{TRANSPORT_KEYWORD.capitalize()} type not defined"
1633 )
1634 transport_type, *transport_options = transport_args
1635
1636 keywords = {"compression", "crypto", LINK_KEYWORD}
1637 parsed_options = parse_args.group_by_keywords(
1638 transport_options,
1639 keywords,
1640 implicit_first_keyword=TRANSPORT_DEFAULT_SECTION,
1641 )
1642 options: dict[str, dict[str, str] | list[dict[str, str]]] = {
1643 section: KeyValueParser(
1644 parsed_options.get_args_flat(section)
1645 ).get_unique()
1646 for section in keywords | {TRANSPORT_DEFAULT_SECTION}
1647 if section != LINK_KEYWORD
1648 }
1649 options[LINK_KEYWORD] = [
1650 KeyValueParser(link_options).get_unique()
1651 for link_options in parsed_options.get_args_groups(LINK_KEYWORD)
1652 ]
1653
1654 return transport_type, options
1655
1656
1657 def cluster_setup(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1658 """
1659 Options:
1660 * --wait - only effective when used with --start
1661 * --start - start cluster
1662 * --enable - enable cluster
1663 * --force - some validation issues and unresolvable addresses are treated
1664 as warnings
1665 * --no-keys-sync - do not create and distribute corosync and pacemaker
1666 authkeys
1667 * --no-cluster-uuid - do not generate a cluster UUID during setup
1668 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
1669 * --overwrite - allow overwriting existing files
1670 """
1671 is_local = modifiers.is_specified("--corosync_conf")
1672
1673 allowed_options_common = ["--force", "--no-cluster-uuid"]
1674 allowed_options_live = [
1675 "--wait",
1676 "--start",
1677 "--enable",
1678 "--no-keys-sync",
1679 ]
1680 allowed_options_local = ["--corosync_conf", "--overwrite"]
1681 modifiers.ensure_only_supported(
1682 *(
1683 allowed_options_common
1684 + allowed_options_live
1685 + allowed_options_local
1686 ),
1687 )
1688 if is_local and modifiers.is_specified_any(allowed_options_live):
1689 raise CmdLineInputError(
1690 f"Cannot specify any of {format_list(allowed_options_live)} "
1691 "when '--corosync_conf' is specified"
1692 )
1693 if not is_local and modifiers.is_specified("--overwrite"):
1694 raise CmdLineInputError(
1695 "Cannot specify '--overwrite' when '--corosync_conf' is not "
1696 "specified"
1697 )
1698
1699 if len(argv) < 2:
1700 raise CmdLineInputError()
1701 cluster_name, *argv = argv
1702 keywords = [TRANSPORT_KEYWORD, "totem", "quorum"]
1703 parsed_args = parse_args.group_by_keywords(
1704 argv, keywords, implicit_first_keyword="nodes"
1705 )
1706 parsed_args.ensure_unique_keywords()
1707 nodes = [
1708 _parse_node_options(node, options)
1709 for node, options in parse_args.split_list_by_any_keywords(
1710 parsed_args.get_args_flat("nodes"), "node name"
1711 ).items()
1712 ]
1713
1714 transport_type = None
1715 transport_options: dict[str, dict[str, str] | list[dict[str, str]]] = {}
1716
1717 if parsed_args.has_keyword(TRANSPORT_KEYWORD):
1718 transport_type, transport_options = _parse_transport(
1719 parsed_args.get_args_flat(TRANSPORT_KEYWORD)
1720 )
1721
1722 force_flags = []
1723 if modifiers.get("--force"):
1724 force_flags.append(reports.codes.FORCE)
1725
1726 totem_options = KeyValueParser(
1727 parsed_args.get_args_flat("totem")
1728 ).get_unique()
1729 quorum_options = KeyValueParser(
1730 parsed_args.get_args_flat("quorum")
1731 ).get_unique()
1732
1733 if not is_local:
1734 lib.cluster.setup(
1735 cluster_name,
1736 nodes,
1737 transport_type=transport_type,
1738 transport_options=transport_options.get(
1739 TRANSPORT_DEFAULT_SECTION, {}
1740 ),
1741 link_list=transport_options.get(LINK_KEYWORD, []),
1742 compression_options=transport_options.get("compression", {}),
1743 crypto_options=transport_options.get("crypto", {}),
1744 totem_options=totem_options,
1745 quorum_options=quorum_options,
1746 wait=modifiers.get("--wait"),
1747 start=modifiers.get("--start"),
1748 enable=modifiers.get("--enable"),
1749 no_keys_sync=modifiers.get("--no-keys-sync"),
1750 no_cluster_uuid=modifiers.is_specified("--no-cluster-uuid"),
1751 force_flags=force_flags,
1752 )
1753 return
1754
1755 corosync_conf_data = lib.cluster.setup_local(
1756 cluster_name,
1757 nodes,
1758 transport_type=transport_type,
1759 transport_options=transport_options.get(TRANSPORT_DEFAULT_SECTION, {}),
1760 link_list=transport_options.get(LINK_KEYWORD, []),
1761 compression_options=transport_options.get("compression", {}),
1762 crypto_options=transport_options.get("crypto", {}),
1763 totem_options=totem_options,
1764 quorum_options=quorum_options,
1765 no_cluster_uuid=modifiers.is_specified("--no-cluster-uuid"),
1766 force_flags=force_flags,
1767 )
1768
1769 corosync_conf_file = pcs_file.RawFile(
1770 file_metadata.for_file_type(
1771 file_type_codes.COROSYNC_CONF, modifiers.get("--corosync_conf")
1772 )
1773 )
1774 overwrite = modifiers.is_specified("--overwrite")
1775 try:
1776 corosync_conf_file.write(corosync_conf_data, can_overwrite=overwrite)
1777 except pcs_file.FileAlreadyExists as e:
1778 utils.err(
1779 reports.messages.FileAlreadyExists(
1780 e.metadata.file_type_code,
1781 e.metadata.path,
1782 ).message
1783 + ", use --overwrite to overwrite existing file(s)"
1784 )
1785 except pcs_file.RawFileError as e:
1786 utils.err(
1787 reports.messages.FileIoError(
1788 e.metadata.file_type_code,
1789 e.action,
1790 e.reason,
1791 file_path=e.metadata.path,
1792 ).message
1793 )
1794
1795
1796 def config_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
1797 """
1798 Options:
1799 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
1800 """
1801 modifiers.ensure_only_supported("--corosync_conf")
1802 parsed_args = parse_args.group_by_keywords(
1803 argv,
1804 ["transport", "compression", "crypto", "totem"],
1805 )
1806
1807 transport_options = KeyValueParser(
1808 parsed_args.get_args_flat("transport")
1809 ).get_unique()
1810 compression_options = KeyValueParser(
1811 parsed_args.get_args_flat("compression")
1812 ).get_unique()
1813 crypto_options = KeyValueParser(
1814 parsed_args.get_args_flat("crypto")
1815 ).get_unique()
1816 totem_options = KeyValueParser(
1817 parsed_args.get_args_flat("totem")
1818 ).get_unique()
1819
1820 if not modifiers.is_specified("--corosync_conf"):
1821 lib.cluster.config_update(
1822 transport_options,
1823 compression_options,
1824 crypto_options,
1825 totem_options,
1826 )
1827 return
1828
1829 _corosync_conf_local_cmd_call(
1830 modifiers.get("--corosync_conf"),
1831 lambda corosync_conf_content: lib.cluster.config_update_local(
1832 corosync_conf_content,
1833 transport_options,
1834 compression_options,
1835 crypto_options,
1836 totem_options,
1837 ),
1838 )
1839
1840
1841 def _format_options(label: str, options: Mapping[str, str]) -> list[str]:
1842 output = []
1843 if options:
1844 output.append(f"{label}:")
1845 output.extend(
1846 indent([f"{opt}: {val}" for opt, val in sorted(options.items())])
1847 )
1848 return output
1849
1850
1851 def _format_nodes(nodes: Iterable[CorosyncNodeDto]) -> list[str]:
1852 output = ["Nodes:"]
1853 for node in sorted(nodes, key=lambda node: node.name):
1854 node_attrs = [
1855 f"Link {addr.link} address: {addr.addr}"
1856 for addr in sorted(node.addrs, key=lambda addr: addr.link)
1857 ] + [f"nodeid: {node.nodeid}"]
1858 output.extend(indent([f"{node.name}:"] + indent(node_attrs)))
1859 return output
1860
1861
1862 def config_show(
1863 lib: Any, argv: Argv, modifiers: parse_args.InputModifiers
1864 ) -> None:
1865 """
1866 Options:
1867 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
1868 * --output-format - supported formats: text, cmd, json
1869 """
1870 modifiers.ensure_only_supported(
1871 "--corosync_conf", output_format_supported=True
1872 )
1873 if argv:
1874 raise CmdLineInputError()
1875 output_format = modifiers.get_output_format()
1876 corosync_conf_dto = lib.cluster.get_corosync_conf_struct()
1877 if output_format == OUTPUT_FORMAT_VALUE_CMD:
1878 if corosync_conf_dto.quorum_device is not None:
1879 warn(
1880 "Quorum device configuration detected but not yet supported by "
1881 "this command."
1882 )
1883 output = " \\\n".join(_config_get_cmd(corosync_conf_dto))
1884 elif output_format == OUTPUT_FORMAT_VALUE_JSON:
1885 output = json.dumps(dto.to_dict(corosync_conf_dto))
1886 else:
1887 output = "\n".join(_config_get_text(corosync_conf_dto))
1888 print(output)
1889
1890
1891 def _config_get_text(corosync_conf: CorosyncConfDto) -> list[str]:
1892 lines = [f"Cluster Name: {corosync_conf.cluster_name}"]
1893 if corosync_conf.cluster_uuid:
1894 lines.append(f"Cluster UUID: {corosync_conf.cluster_uuid}")
1895 lines.append(f"Transport: {corosync_conf.transport.lower()}")
1896 lines.extend(_format_nodes(corosync_conf.nodes))
1897 if corosync_conf.links_options:
1898 lines.append("Links:")
1899 for linknum, link_options in sorted(
1900 corosync_conf.links_options.items()
1901 ):
1902 lines.extend(
1903 indent(_format_options(f"Link {linknum}", link_options))
1904 )
1905
1906 lines.extend(
1907 _format_options("Transport Options", corosync_conf.transport_options)
1908 )
1909 lines.extend(
1910 _format_options(
1911 "Compression Options", corosync_conf.compression_options
1912 )
1913 )
1914 lines.extend(
1915 _format_options("Crypto Options", corosync_conf.crypto_options)
1916 )
1917 lines.extend(_format_options("Totem Options", corosync_conf.totem_options))
1918 lines.extend(
1919 _format_options("Quorum Options", corosync_conf.quorum_options)
1920 )
1921 if corosync_conf.quorum_device:
1922 lines.append(f"Quorum Device: {corosync_conf.quorum_device.model}")
1923 lines.extend(
1924 indent(
1925 _format_options(
1926 "Options", corosync_conf.quorum_device.generic_options
1927 )
1928 )
1929 )
1930 lines.extend(
1931 indent(
1932 _format_options(
1933 "Model Options",
1934 corosync_conf.quorum_device.model_options,
1935 )
1936 )
1937 )
1938 lines.extend(
1939 indent(
1940 _format_options(
1941 "Heuristics",
1942 corosync_conf.quorum_device.heuristics_options,
1943 )
1944 )
1945 )
1946 return lines
1947
1948
1949 def _corosync_node_to_cmd_line(node: CorosyncNodeDto) -> str:
1950 return " ".join(
1951 [node.name]
1952 + [
1953 f"addr={addr.addr}"
1954 for addr in sorted(node.addrs, key=lambda addr: addr.link)
1955 ]
1956 )
1957
1958
1959 def _section_to_lines(
1960 options: Mapping[str, str], keyword: str | None = None
1961 ) -> list[str]:
1962 output: list[str] = []
1963 if options:
1964 if keyword:
1965 output.append(keyword)
1966 output.extend(
1967 indent([f"{key}={val}" for key, val in sorted(options.items())])
1968 )
1969 return indent(output)
1970
1971
1972 def _config_get_cmd(corosync_conf: CorosyncConfDto) -> list[str]:
1973 lines = [f"pcs cluster setup {corosync_conf.cluster_name}"]
1974 lines += indent(
1975 [
1976 _corosync_node_to_cmd_line(node)
1977 for node in sorted(
1978 corosync_conf.nodes, key=lambda node: node.nodeid
1979 )
1980 ]
1981 )
1982 transport = [
1983 "transport",
1984 str(corosync_conf.transport.value).lower(),
1985 ] + _section_to_lines(corosync_conf.transport_options)
1986 for _, link in sorted(corosync_conf.links_options.items()):
1987 transport.extend(_section_to_lines(link, "link"))
1988 transport.extend(
1989 _section_to_lines(corosync_conf.compression_options, "compression")
1990 )
1991 transport.extend(_section_to_lines(corosync_conf.crypto_options, "crypto"))
1992 lines.extend(indent(transport))
1993 lines.extend(_section_to_lines(corosync_conf.totem_options, "totem"))
1994 lines.extend(_section_to_lines(corosync_conf.quorum_options, "quorum"))
1995 if not corosync_conf.cluster_uuid:
1996 lines.extend(indent(["--no-cluster-uuid"]))
1997 return lines
1998
1999
2000 def _parse_add_node(argv: Argv) -> dict[str, str | list[str]]:
2001 DEVICE_KEYWORD = "device"
2002 WATCHDOG_KEYWORD = "watchdog"
2003 hostname, *argv = argv
2004 node_dict = _parse_node_options(
2005 hostname,
2006 argv,
2007 additional_options={DEVICE_KEYWORD, WATCHDOG_KEYWORD},
2008 additional_repeatable_options={DEVICE_KEYWORD},
2009 )
2010 if DEVICE_KEYWORD in node_dict:
2011 node_dict[f"{DEVICE_KEYWORD}s"] = node_dict[DEVICE_KEYWORD]
2012 del node_dict[DEVICE_KEYWORD]
2013 return node_dict
2014
2015
2016 def node_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2017 """
2018 Options:
2019 * --wait - wait until new node will start up, effective only when --start
2020 is specified
2021 * --start - start new node
2022 * --enable - enable new node
2023 * --force - treat validation issues and not resolvable addresses as
2024 warnings instead of errors
2025 * --skip-offline - skip unreachable nodes
2026 * --no-watchdog-validation - do not validatate watchdogs
2027 * --request-timeout - HTTP request timeout
2028 """
2029 modifiers.ensure_only_supported(
2030 "--wait",
2031 "--start",
2032 "--enable",
2033 "--force",
2034 "--skip-offline",
2035 "--no-watchdog-validation",
2036 "--request-timeout",
2037 )
2038 if not argv:
2039 raise CmdLineInputError()
2040
2041 node_dict = _parse_add_node(argv)
2042
2043 force_flags = []
2044 if modifiers.get("--force"):
2045 force_flags.append(reports.codes.FORCE)
2046 if modifiers.get("--skip-offline"):
2047 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2048
2049 lib.cluster.add_nodes(
2050 nodes=[node_dict],
2051 wait=modifiers.get("--wait"),
2052 start=modifiers.get("--start"),
2053 enable=modifiers.get("--enable"),
2054 no_watchdog_validation=modifiers.get("--no-watchdog-validation"),
2055 force_flags=force_flags,
2056 )
2057
2058
2059 def remove_nodes_from_cib(
2060 lib: Any, argv: Argv, modifiers: InputModifiers
2061 ) -> None:
2062 """
2063 Options: no options
2064 """
2065 modifiers.ensure_only_supported()
2066 if not argv:
2067 raise CmdLineInputError("No nodes specified")
2068 lib.cluster.remove_nodes_from_cib(argv)
2069
2070
2071 def link_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2072 """
2073 Options:
2074 * --force - treat validation issues and not resolvable addresses as
2075 warnings instead of errors
2076 * --skip-offline - skip unreachable nodes
2077 * --request-timeout - HTTP request timeout
2078 """
2079 modifiers.ensure_only_supported(
2080 "--force", "--request-timeout", "--skip-offline"
2081 )
2082 if not argv:
2083 raise CmdLineInputError()
2084
2085 force_flags = []
2086 if modifiers.get("--force"):
2087 force_flags.append(reports.codes.FORCE)
2088 if modifiers.get("--skip-offline"):
2089 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2090
2091 parsed = parse_args.group_by_keywords(
2092 argv, {"options"}, implicit_first_keyword="nodes"
2093 )
2094 parsed.ensure_unique_keywords()
2095
2096 lib.cluster.add_link(
2097 KeyValueParser(parsed.get_args_flat("nodes")).get_unique(),
2098 KeyValueParser(parsed.get_args_flat("options")).get_unique(),
2099 force_flags=force_flags,
2100 )
2101
2102
2103 def link_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2104 """
2105 Options:
2106 * --skip-offline - skip unreachable nodes
2107 * --request-timeout - HTTP request timeout
2108 """
2109 modifiers.ensure_only_supported("--request-timeout", "--skip-offline")
2110 if not argv:
2111 raise CmdLineInputError()
2112
2113 force_flags = []
2114 if modifiers.get("--skip-offline"):
2115 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2116
2117 lib.cluster.remove_links(argv, force_flags=force_flags)
2118
2119
2120 def link_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2121 """
2122 Options:
2123 * --force - treat validation issues and not resolvable addresses as
2124 warnings instead of errors
2125 * --skip-offline - skip unreachable nodes
2126 * --request-timeout - HTTP request timeout
2127 """
2128 modifiers.ensure_only_supported(
2129 "--force", "--request-timeout", "--skip-offline"
2130 )
2131 if len(argv) < 2:
2132 raise CmdLineInputError()
2133
2134 force_flags = []
2135 if modifiers.get("--force"):
2136 force_flags.append(reports.codes.FORCE)
2137 if modifiers.get("--skip-offline"):
2138 force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
2139
2140 linknumber = argv[0]
2141 parsed = parse_args.group_by_keywords(
2142 argv[1:], {"options"}, implicit_first_keyword="nodes"
2143 )
2144 parsed.ensure_unique_keywords()
2145
2146 lib.cluster.update_link(
2147 linknumber,
2148 KeyValueParser(parsed.get_args_flat("nodes")).get_unique(),
2149 KeyValueParser(parsed.get_args_flat("options")).get_unique(),
2150 force_flags=force_flags,
2151 )
2152
2153
2154 def generate_uuid(lib: Any, argv: Argv, modifiers: InputModifiers) -> None:
2155 """
2156 Options:
2157 * --force - allow to rewrite an existing UUID in corosync.conf
2158 * --corosync_conf - corosync.conf file path, do not talk to cluster nodes
2159 """
2160 modifiers.ensure_only_supported("--force", "--corosync_conf")
2161 if argv:
2162 raise CmdLineInputError()
2163
2164 force_flags = []
2165 if modifiers.get("--force"):
2166 force_flags.append(reports.codes.FORCE)
2167
2168 if not modifiers.is_specified("--corosync_conf"):
2169 lib.cluster.generate_cluster_uuid(force_flags=force_flags)
2170 return
2171
2172 _corosync_conf_local_cmd_call(
2173 modifiers.get("--corosync_conf"),
2174 lambda corosync_conf_content: lib.cluster.generate_cluster_uuid_local(
2175 corosync_conf_content, force_flags=force_flags
2176 ),
2177 )
2178