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