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