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