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