1    	import base64
2    	import getpass
3    	import json
4    	import logging
5    	import os
6    	import re
7    	import signal
8    	import subprocess
9    	import sys
10   	import tarfile
11   	import tempfile
12   	import threading
13   	import time
14   	import xml.dom.minidom
15   	from functools import lru_cache
16   	from io import BytesIO
17   	from textwrap import dedent
18   	from typing import Any, cast
19   	from urllib.parse import urlencode
20   	from xml.dom.minidom import Document as DomDocument
21   	from xml.dom.minidom import parseString
22   	
23   	import pcs.cli.booth.env
24   	import pcs.lib.corosync.config_parser as corosync_conf_parser
25   	from pcs import settings, usage
26   	from pcs.cli.cluster_property.output import PropertyConfigurationFacade
27   	from pcs.cli.common import middleware
28   	from pcs.cli.common.env_cli import Env
29   	from pcs.cli.common.errors import CmdLineInputError
30   	from pcs.cli.common.lib_wrapper import Library
31   	from pcs.cli.common.parse_args import InputModifiers
32   	from pcs.cli.common.tools import print_to_stderr, timeout_to_seconds_legacy
33   	from pcs.cli.file import metadata as cli_file_metadata
34   	from pcs.cli.reports import ReportProcessorToConsole
35   	from pcs.cli.reports import output as reports_output
36   	from pcs.cli.reports.messages import report_item_msg_from_dto
37   	from pcs.common import const, file_type_codes
38   	from pcs.common import file as pcs_file
39   	from pcs.common import pacemaker as common_pacemaker
40   	from pcs.common import pcs_pycurl as pycurl
41   	from pcs.common.host import PcsKnownHost
42   	from pcs.common.pacemaker.resource.operations import (
43   	    OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME,
44   	)
45   	from pcs.common.reports import ReportItemList, ReportItemSeverity
46   	from pcs.common.reports.messages import CibUpgradeFailedToMinimalRequiredVersion
47   	from pcs.common.reports.utils import add_context_to_message
48   	from pcs.common.services.errors import ManageServiceError
49   	from pcs.common.services.interfaces import ServiceManagerInterface
50   	from pcs.common.str_tools import format_list
51   	from pcs.common.tools import Version, timeout_to_seconds
52   	from pcs.common.types import StringSequence
53   	from pcs.lib.corosync.config_facade import ConfigFacade as corosync_conf_facade
54   	from pcs.lib.env import LibraryEnvironment
55   	from pcs.lib.errors import LibraryError
56   	from pcs.lib.external import CommandRunner, is_proxy_set
57   	from pcs.lib.file.instance import FileInstance as LibFileInstance
58   	from pcs.lib.host.config.facade import Facade as KnownHostsFacade
59   	from pcs.lib.interface.config import ParserErrorException
60   	from pcs.lib.pacemaker.values import validate_id
61   	from pcs.lib.services import get_service_manager as _get_service_manager
62   	from pcs.lib.services import service_exception_to_report
63   	
64   	# usefile & filename variables are set in pcs module
65   	usefile = False
66   	filename = ""
67   	# Note: not properly typed
68   	pcs_options: dict[Any, Any] = {}
69   	
70   	
71   	def _getValidateWithVersion(dom) -> Version:
72   	    """
73   	    Commandline options: no options
74   	    """
75   	    cib = dom.getElementsByTagName("cib")
76   	    if len(cib) != 1:
77   	        err("Bad cib")
78   	
79   	    cib = cib[0]
80   	
81   	    version = cib.getAttribute("validate-with")
82   	    r = re.compile(r"pacemaker-(\d+)\.(\d+)\.?(\d+)?")
83   	    m = r.match(version)
84   	    if m is None:
85   	        raise AssertionError()
86   	    major = int(m.group(1))
87   	    minor = int(m.group(2))
88   	    rev = int(m.group(3) or 0)
89   	    return Version(major, minor, rev)
90   	
91   	
92   	def isCibVersionSatisfied(cib_dom, required_version: Version) -> bool:
93   	    if not isinstance(cib_dom, DomDocument):
94   	        cib_dom = cib_dom.ownerDocument
95   	    return _getValidateWithVersion(cib_dom) >= required_version
96   	
97   	
98   	# Check the current pacemaker version in cib and upgrade it if necessary
99   	# Returns False if not upgraded and True if upgraded
100  	def _checkAndUpgradeCIB(required_version: Version) -> bool:
101  	    """
102  	    Commandline options:
103  	      * -f - CIB file
104  	    """
105  	    if isCibVersionSatisfied(get_cib_dom(), required_version):
106  	        return False
107  	    cluster_upgrade()
108  	    return True
109  	
110  	
111  	def cluster_upgrade():
112  	    """
113  	    Commandline options:
114  	      * -f - CIB file
115  	    """
116  	    output, retval = run(["cibadmin", "--upgrade", "--force"])
117  	    if retval != 0:
118  	        err("unable to upgrade cluster: %s" % output)
119  	    if (
120  	        output.strip()
121  	        == "Upgrade unnecessary: Schema is already the latest available"
122  	    ):
123  	        return
124  	    print_to_stderr("Cluster CIB has been upgraded to latest version")
125  	
126  	
127  	def cluster_upgrade_to_version(required_version: Version) -> Any:
128  	    """
129  	    Commandline options:
130  	      * -f - CIB file
131  	    """
132  	    _checkAndUpgradeCIB(required_version)
133  	    dom = get_cib_dom()
134  	    current_version = _getValidateWithVersion(dom)
135  	    if current_version < required_version:
136  	        err(
137  	            CibUpgradeFailedToMinimalRequiredVersion(
138  	                str(current_version),
139  	                str(required_version),
140  	            ).message
141  	        )
142  	    return dom
143  	
144  	
145  	# Check status of node
146  	def checkStatus(node):
147  	    """
148  	    Commandline options:
149  	      * --request-timeout - timeout for HTTP requests
150  	    """
151  	    return sendHTTPRequest(
152  	        node, "remote/status", urlencode({"version": "2"}), False, False
153  	    )
154  	
155  	
156  	# Check and see if we're authorized (faster than a status check)
157  	def checkAuthorization(node):
158  	    """
159  	    Commandline options:
160  	      * --request-timeout - timeout for HTTP requests
161  	    """
162  	    return sendHTTPRequest(node, "remote/check_auth", None, False, False)
163  	
164  	
165  	def get_uid_gid_file_name(uid, gid):
166  	    """
167  	    Commandline options: no options
168  	    """
169  	    return "pcs-uidgid-%s-%s" % (uid, gid)
170  	
171  	
172  	# Reads in uid file and returns dict of values {'uid':'theuid', 'gid':'thegid'}
173  	def read_uid_gid_file(uidgid_filename):
174  	    """
175  	    Commandline options: no options
176  	    """
177  	    uidgid = {}
178  	    with open(
179  	        os.path.join(settings.corosync_uidgid_dir, uidgid_filename), "r"
180  	    ) as myfile:
181  	        data = myfile.read().split("\n")
182  	    in_uidgid = False
183  	    for data_line in data:
184  	        line = re.sub(r"#.*", "", data_line)
185  	        if not in_uidgid:
186  	            if re.search(r"uidgid.*{", line):
187  	                in_uidgid = True
188  	            else:
189  	                continue
190  	        matches = re.search(r"uid:\s*(\S+)", line)
191  	        if matches:
192  	            uidgid["uid"] = matches.group(1)
193  	
194  	        matches = re.search(r"gid:\s*(\S+)", line)
195  	        if matches:
196  	            uidgid["gid"] = matches.group(1)
197  	
198  	    return uidgid
199  	
200  	
201  	def get_uidgid_file_content(
202  	    uid: str | None = None, gid: str | None = None
203  	) -> str | None:
204  	    if not uid and not gid:
205  	        return None
206  	    uid_gid_lines = []
207  	    if uid:
208  	        uid_gid_lines.append(f"  uid: {uid}")
209  	    if gid:
210  	        uid_gid_lines.append(f"  gid: {gid}")
211  	    return dedent(
212  	        """\
213  	        uidgid {{
214  	        {uid_gid_keys}
215  	        }}
216  	        """
217  	    ).format(uid_gid_keys="\n".join(uid_gid_lines))
218  	
219  	
220  	def write_uid_gid_file(uid, gid):
221  	    """
222  	    Commandline options: no options
223  	    """
224  	    orig_filename = get_uid_gid_file_name(uid, gid)
225  	    uidgid_filename = orig_filename
226  	    counter = 0
227  	    if find_uid_gid_files(uid, gid):
228  	        err("uidgid file with uid=%s and gid=%s already exists" % (uid, gid))
229  	
230  	    while os.path.exists(
231  	        os.path.join(settings.corosync_uidgid_dir, uidgid_filename)
232  	    ):
233  	        counter = counter + 1
234  	        uidgid_filename = orig_filename + "-" + str(counter)
235  	
236  	    data = get_uidgid_file_content(uid, gid)
237  	    if data:
238  	        with open(
239  	            os.path.join(settings.corosync_uidgid_dir, uidgid_filename), "w"
240  	        ) as uidgid_file:
241  	            uidgid_file.write(data)
242  	
243  	
244  	def find_uid_gid_files(uid, gid):
245  	    """
246  	    Commandline options: no options
247  	    """
248  	    if uid == "" and gid == "":
249  	        return []
250  	
251  	    found_files = []
252  	    uid_gid_files = os.listdir(settings.corosync_uidgid_dir)
253  	    for uidgid_file in uid_gid_files:
254  	        uid_gid_dict = read_uid_gid_file(uidgid_file)
255  	        if ("uid" in uid_gid_dict and uid == "") or (
256  	            "uid" not in uid_gid_dict and uid != ""
257  	        ):
258  	            continue
259  	        if ("gid" in uid_gid_dict and gid == "") or (
260  	            "gid" not in uid_gid_dict and gid != ""
261  	        ):
262  	            continue
263  	        if "uid" in uid_gid_dict and uid != uid_gid_dict["uid"]:
264  	            continue
265  	        if "gid" in uid_gid_dict and gid != uid_gid_dict["gid"]:
266  	            continue
267  	
268  	        found_files.append(uidgid_file)
269  	
270  	    return found_files
271  	
272  	
273  	# Removes all uid/gid files with the specified uid/gid, returns false if we
274  	# couldn't find one
275  	def remove_uid_gid_file(uid, gid):
276  	    """
277  	    Commandline options: no options
278  	    """
279  	    if uid == "" and gid == "":
280  	        return False
281  	
282  	    file_removed = False
283  	    for uidgid_file in find_uid_gid_files(uid, gid):
284  	        os.remove(os.path.join(settings.corosync_uidgid_dir, uidgid_file))
285  	        file_removed = True
286  	
287  	    return file_removed
288  	
289  	
290  	# DEPRECATED
291  	# This function has been replaced in all pcs code and has been kept solely for
292  	# the purpose of read_known_hosts_file_not_cached function in this module. Once
293  	# that function is cleaned up, this function should be removed.
294  	def _process_library_reports(
295  	    report_item_list: ReportItemList,
296  	    exit_on_error: bool = True,
297  	    include_debug: bool = True,
298  	) -> None:
299  	    if not report_item_list:
300  	        raise reports_output.error(
301  	            "Errors have occurred, therefore pcs is unable to continue"
302  	        )
303  	
304  	    critical_error = False
305  	    for report_item in report_item_list:
306  	        report_dto = report_item.to_dto()
307  	        severity = report_dto.severity.level
308  	
309  	        if severity == ReportItemSeverity.DEBUG and not include_debug:
310  	            continue
311  	
312  	        cli_report_msg = report_item_msg_from_dto(report_dto.message)
313  	        msg = add_context_to_message(cli_report_msg.message, report_dto.context)
314  	
315  	        if severity == ReportItemSeverity.WARNING:
316  	            reports_output.warn(msg)
317  	            continue
318  	
319  	        if severity == ReportItemSeverity.DEPRECATION:
320  	            reports_output.deprecation_warning(msg)
321  	            continue
322  	
323  	        if severity != ReportItemSeverity.ERROR:
324  	            reports_output.print_to_stderr(msg)
325  	            continue
326  	
327  	        reports_output.error(
328  	            add_context_to_message(
329  	                cli_report_msg.get_message_with_force_text(
330  	                    report_item.severity.force_code
331  	                ),
332  	                report_dto.context,
333  	            )
334  	        )
335  	        critical_error = True
336  	
337  	    if critical_error and exit_on_error:
338  	        sys.exit(1)
339  	
340  	
341  	@lru_cache
342  	def read_known_hosts_file() -> dict[str, PcsKnownHost]:
343  	    return read_known_hosts_file_not_cached()
344  	
345  	
346  	# TODO
347  	# This is the last function where _process_library_reports was not replaced
348  	# with pcs.cli.reports.processor.report_and_exit_on_error. The problem is, that
349  	# this function is meant to be used in CLI layer only, and despite that it is
350  	# being used in pcs.daemon.async_tasks.worker.executor. Replacing
351  	# _process_library_reports with report_and_exit_on_error cannot be done, as
352  	# they both use different report processors and that would break the daemon.
353  	# Steps:
354  	# 1. Write known hosts file getter designed specifically for the daemon context.
355  	#    This should have been done in the first place.
356  	# 2. Replace _process_library_reports with report_and_exit_on_error.
357  	# 3. Drop _process_library_reports.
358  	def read_known_hosts_file_not_cached() -> dict[str, PcsKnownHost]:
359  	    """
360  	    Commandline options: no options
361  	    """
362  	    try:
363  	        if os.getuid() != 0:
364  	            known_hosts_raw_file = pcs_file.RawFile(
365  	                cli_file_metadata.for_file_type(file_type_codes.PCS_KNOWN_HOSTS)
366  	            )
367  	            # json.loads handles bytes, it expects utf-8, 16 or 32 encoding
368  	            known_hosts_struct = json.loads(known_hosts_raw_file.read())
369  	            # TODO use known hosts facade for getting info from json struct once the
370  	            # facade exists
371  	            return {
372  	                name: PcsKnownHost.from_known_host_file_dict(name, host)
373  	                for name, host in known_hosts_struct["known_hosts"].items()
374  	            }
375  	        # TODO remove
376  	        # This is here to provide known-hosts to functions not yet
377  	        # overhauled to pcs.lib. Cli should never read known hosts from
378  	        # /var/lib/pcsd/.
379  	        known_hosts_instance = LibFileInstance.for_known_hosts()
380  	        known_hosts_facade = cast(
381  	            KnownHostsFacade, known_hosts_instance.read_to_facade()
382  	        )
383  	        return known_hosts_facade.known_hosts
384  	
385  	    except LibraryError as e:
386  	        # TODO remove
387  	        # This is here to provide known-hosts to functions not yet
388  	        # overhauled to pcs.lib. Cli should never read known hosts from
389  	        # /var/lib/pcsd/.
390  	        _process_library_reports(list(e.args))
391  	    except ParserErrorException as e:
392  	        # TODO remove
393  	        # This is here to provide known-hosts to functions not yet
394  	        # overhauled to pcs.lib. Cli should never read known hosts from
395  	        # /var/lib/pcsd/.
396  	        _process_library_reports(
397  	            known_hosts_instance.parser_exception_to_report_list(e)
398  	        )
399  	    except pcs_file.RawFileError as e:
400  	        reports_output.warn("Unable to read the known-hosts file: " + e.reason)
401  	    except json.JSONDecodeError as e:
402  	        reports_output.warn(f"Unable to parse the known-hosts file: {e}")
403  	    except (TypeError, KeyError):
404  	        reports_output.warn("Warning: Unable to parse the known-hosts file.")
405  	    return {}
406  	
407  	
408  	def repeat_if_timeout(send_http_request_function, repeat_count=15):
409  	    """
410  	    Commandline options: no options
411  	    NOTE: callback send_http_request_function may use --request-timeout
412  	    """
413  	
414  	    def repeater(node, *args, **kwargs):
415  	        repeats_left = repeat_count
416  	        while True:
417  	            retval, output = send_http_request_function(node, *args, **kwargs)
418  	            if (
419  	                retval != 2
420  	                or "Operation timed out" not in output
421  	                or repeats_left < 1
422  	            ):
423  	                # did not timed out OR repeat limit exceeded
424  	                return retval, output
425  	            repeats_left = repeats_left - 1
426  	            if "--debug" in pcs_options:
427  	                print_to_stderr(f"{node}: {output}, trying again...")
428  	
429  	    return repeater
430  	
431  	
432  	def setCorosyncConfig(node, config):
433  	    """
434  	    Commandline options:
435  	      * --request-timeout - timeout for HTTP requests
436  	    """
437  	    data = urlencode({"corosync_conf": config})
438  	    (status, data) = sendHTTPRequest(node, "remote/set_corosync_conf", data)
439  	    if status != 0:
440  	        err("Unable to set corosync config: {0}".format(data))
441  	
442  	
443  	def getPacemakerNodeStatus(node):
444  	    """
445  	    Commandline options:
446  	      * --request-timeout - timeout for HTTP requests
447  	    """
448  	    return sendHTTPRequest(
449  	        node, "remote/pacemaker_node_status", None, False, False
450  	    )
451  	
452  	
453  	def startCluster(node, quiet=False, timeout=None):
454  	    """
455  	    Commandline options:
456  	      * --request-timeout - timeout for HTTP requests
457  	    """
458  	    return sendHTTPRequest(
459  	        node,
460  	        "remote/cluster_start",
461  	        printResult=False,
462  	        printSuccess=not quiet,
463  	        timeout=timeout,
464  	    )
465  	
466  	
467  	def stopPacemaker(node, quiet=False, force=True):
468  	    """
469  	    Commandline options:
470  	      * --request-timeout - timeout for HTTP requests
471  	    """
472  	    return stopCluster(
473  	        node, pacemaker=True, corosync=False, quiet=quiet, force=force
474  	    )
475  	
476  	
477  	def stopCorosync(node, quiet=False, force=True):
478  	    """
479  	    Commandline options:
480  	      * --request-timeout - timeout for HTTP requests
481  	    """
482  	    return stopCluster(
483  	        node, pacemaker=False, corosync=True, quiet=quiet, force=force
484  	    )
485  	
486  	
487  	def stopCluster(node, quiet=False, pacemaker=True, corosync=True, force=True):
488  	    """
489  	    Commandline options:
490  	      * --request-timeout - timeout for HTTP requests
491  	    """
492  	    data = {}
493  	    timeout = None
494  	    if pacemaker and not corosync:
495  	        data["component"] = "pacemaker"
496  	        timeout = 2 * 60
497  	    elif corosync and not pacemaker:
498  	        data["component"] = "corosync"
499  	    if force:
500  	        data["force"] = 1
501  	    data = urlencode(data)
502  	    return sendHTTPRequest(
503  	        node,
504  	        "remote/cluster_stop",
505  	        data,
506  	        printResult=False,
507  	        printSuccess=not quiet,
508  	        timeout=timeout,
509  	    )
510  	
511  	
512  	def enableCluster(node):
513  	    """
514  	    Commandline options:
515  	      * --request-timeout - timeout for HTTP requests
516  	    """
517  	    return sendHTTPRequest(node, "remote/cluster_enable", None, False, True)
518  	
519  	
520  	def disableCluster(node):
521  	    """
522  	    Commandline options:
523  	      * --request-timeout - timeout for HTTP requests
524  	    """
525  	    return sendHTTPRequest(node, "remote/cluster_disable", None, False, True)
526  	
527  	
528  	def destroyCluster(node, quiet=False):
529  	    """
530  	    Commandline options:
531  	      * --request-timeout - timeout for HTTP requests
532  	    """
533  	    return sendHTTPRequest(
534  	        node, "remote/cluster_destroy", None, not quiet, not quiet
535  	    )
536  	
537  	
538  	def restoreConfig(node, tarball_data):
539  	    """
540  	    Commandline options:
541  	      * --request-timeout - timeout for HTTP requests
542  	    """
543  	    data = urlencode({"tarball": tarball_data})
544  	    return sendHTTPRequest(node, "remote/config_restore", data, False, True)
545  	
546  	
547  	def pauseConfigSyncing(node, delay_seconds=300):
548  	    """
549  	    Commandline options:
550  	      * --request-timeout - timeout for HTTP requests
551  	    """
552  	    data = urlencode({"sync_thread_pause": delay_seconds})
553  	    return sendHTTPRequest(node, "remote/set_sync_options", data, False, False)
554  	
555  	
556  	# Send an HTTP request to a node return a tuple with status, data
557  	# If status is 0 then data contains server response
558  	# Otherwise if non-zero then data contains error message
559  	# Returns a tuple (error, error message)
560  	# 0 = Success,
561  	# 1 = HTTP Error
562  	# 2 = No response,
563  	# 3 = Auth Error
564  	# 4 = Permission denied
565  	def sendHTTPRequest(  # noqa: PLR0912, PLR0915
566  	    host, request, data=None, printResult=True, printSuccess=True, timeout=None
567  	):
568  	    """
569  	    Commandline options:
570  	      * --request-timeout - timeout for HTTP requests
571  	      * --debug
572  	    """
573  	    port = None
574  	    addr = host
575  	    token = None
576  	    known_host = read_known_hosts_file().get(host, None)
577  	    # TODO: do not allow communication with unknown host
578  	    if known_host:
579  	        port = known_host.dest.port
580  	        addr = known_host.dest.addr
581  	        token = known_host.token
582  	    if port is None:
583  	        port = settings.pcsd_default_port
584  	    url = "https://{host}:{port}/{request}".format(
585  	        host="[{0}]".format(addr) if ":" in addr else addr,
586  	        request=request,
587  	        port=port,
588  	    )
589  	    if "--debug" in pcs_options:
590  	        print_to_stderr(f"Sending HTTP Request to: {url}\nData: {data}")
591  	
592  	    def __debug_callback(data_type, debug_data):
593  	        prefixes = {
594  	            pycurl.DEBUG_TEXT: b"* ",
595  	            pycurl.DEBUG_HEADER_IN: b"< ",
596  	            pycurl.DEBUG_HEADER_OUT: b"> ",
597  	            pycurl.DEBUG_DATA_IN: b"<< ",
598  	            pycurl.DEBUG_DATA_OUT: b">> ",
599  	        }
600  	        if data_type in prefixes:
601  	            debug_output.write(prefixes[data_type])
602  	            debug_output.write(debug_data)
603  	            if not debug_data.endswith(b"\n"):
604  	                debug_output.write(b"\n")
605  	
606  	    output = BytesIO()
607  	    debug_output = BytesIO()
608  	    cookies = __get_cookie_list(token)
609  	    if not timeout:
610  	        timeout = settings.default_request_timeout
611  	    timeout = pcs_options.get("--request-timeout", timeout)
612  	
613  	    handler = pycurl.Curl()
614  	    handler.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTPS)
615  	    handler.setopt(pycurl.URL, url.encode("utf-8"))
616  	    handler.setopt(pycurl.WRITEFUNCTION, output.write)
617  	    handler.setopt(pycurl.VERBOSE, 1)
618  	    handler.setopt(pycurl.NOSIGNAL, 1)  # required for multi-threading
619  	    handler.setopt(pycurl.DEBUGFUNCTION, __debug_callback)
620  	    handler.setopt(pycurl.TIMEOUT_MS, int(timeout * 1000))
621  	    handler.setopt(pycurl.SSL_VERIFYHOST, 0)
622  	    handler.setopt(pycurl.SSL_VERIFYPEER, 0)
623  	    handler.setopt(pycurl.HTTPHEADER, ["Expect: "])
624  	    if cookies:
625  	        handler.setopt(pycurl.COOKIE, ";".join(cookies).encode("utf-8"))
626  	    if data:
627  	        handler.setopt(pycurl.COPYPOSTFIELDS, data.encode("utf-8"))
628  	    try:
629  	        handler.perform()
630  	        response_data = output.getvalue().decode("utf-8")
631  	        response_code = handler.getinfo(pycurl.RESPONSE_CODE)
632  	        if printResult or printSuccess:
633  	            print_to_stderr(host + ": " + response_data.strip())
634  	        if "--debug" in pcs_options:
635  	            print_to_stderr(
636  	                "Response Code: {response_code}\n"
637  	                "--Debug Response Start--\n"
638  	                "{response_data}\n"
639  	                "--Debug Response End--\n"
640  	                "Communication debug info for calling: {url}\n"
641  	                "--Debug Communication Output Start--\n"
642  	                "{debug_comm_output}\n"
643  	                "--Debug Communication Output End--".format(
644  	                    response_code=response_code,
645  	                    response_data=response_data,
646  	                    url=url,
647  	                    debug_comm_output=debug_output.getvalue().decode(
648  	                        "utf-8", "ignore"
649  	                    ),
650  	                )
651  	            )
652  	
653  	        if response_code == 401:
654  	            output = (
655  	                3,
656  	                (
657  	                    "Unable to authenticate to {node} - (HTTP error: {code}), "
658  	                    "try running 'pcs host auth {node}'"
659  	                ).format(node=host, code=response_code),
660  	            )
661  	        elif response_code == 403:
662  	            output = (
663  	                4,
664  	                "{node}: Permission denied - (HTTP error: {code})".format(
665  	                    node=host, code=response_code
666  	                ),
667  	            )
668  	        elif response_code >= 400:
669  	            output = (
670  	                1,
671  	                "Error connecting to {node} - (HTTP error: {code})".format(
672  	                    node=host, code=response_code
673  	                ),
674  	            )
675  	        else:
676  	            output = (0, response_data)
677  	
678  	        if printResult and output[0] != 0:
679  	            print_to_stderr(output[1])
680  	
681  	        return output
682  	    except pycurl.error as e:
683  	        if is_proxy_set(os.environ):
684  	            reports_output.warn(
685  	                "Proxy is set in environment variables, try disabling it"
686  	            )
687  	        dummy_errno, reason = e.args
688  	        if "--debug" in pcs_options:
689  	            print_to_stderr(f"Response Reason: {reason}")
690  	        msg = (
691  	            "Unable to connect to {host}, check if pcsd is running there or try "
692  	            "setting higher timeout with --request-timeout option ({reason})"
693  	        ).format(host=host, reason=reason)
694  	        if printResult:
695  	            print_to_stderr(msg)
696  	        return (2, msg)
697  	
698  	
699  	def __get_cookie_list(token):
700  	    """
701  	    Commandline options: no options
702  	    """
703  	    cookies = []
704  	    if token:
705  	        cookies.append("token=" + token)
706  	    if os.geteuid() == 0:
707  	        for name in ("CIB_user", "CIB_user_groups"):
708  	            if name in os.environ and os.environ[name].strip():
709  	                value = os.environ[name].strip()
710  	                # Let's be safe about characters in env variables and do base64.
711  	                # We cannot do it for CIB_user however to be backward compatible
712  	                # so we at least remove disallowed characters.
713  	                if name == "CIB_user":
714  	                    value = re.sub(r"[^!-~]", "", value).replace(";", "")
715  	                else:
716  	                    # python3 requires the value to be bytes not str
717  	                    value = base64.b64encode(value.encode("utf8")).decode(
718  	                        "utf-8"
719  	                    )
720  	                cookies.append("{0}={1}".format(name, value))
721  	    return cookies
722  	
723  	
724  	def get_corosync_conf_facade(conf_text=None):
725  	    """
726  	    Commandline options:
727  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
728  	        settings
729  	    """
730  	    try:
731  	        return corosync_conf_facade(
732  	            corosync_conf_parser.Parser.parse(
733  	                (getCorosyncConf() if conf_text is None else conf_text).encode(
734  	                    "utf-8"
735  	                )
736  	            )
737  	        )
738  	    except corosync_conf_parser.CorosyncConfParserException as e:
739  	        return err("Unable to parse corosync.conf: %s" % e)
740  	
741  	
742  	def hasCorosyncConf():
743  	    """
744  	    Commandline options:
745  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
746  	        settings
747  	    """
748  	    return os.path.isfile(settings.corosync_conf_file)
749  	
750  	
751  	def getCorosyncConf():
752  	    """
753  	    Commandline options:
754  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
755  	        settings
756  	    """
757  	    corosync_conf_content = None
758  	    try:
759  	        with open(
760  	            settings.corosync_conf_file, "r", encoding="utf-8"
761  	        ) as corosync_conf_file:
762  	            corosync_conf_content = corosync_conf_file.read()
763  	    except OSError as e:
764  	        err("Unable to read %s: %s" % (settings.corosync_conf_file, e.strerror))
765  	    return corosync_conf_content
766  	
767  	
768  	def getCorosyncActiveNodes():
769  	    """
770  	    Commandline options: no options
771  	    """
772  	    output, retval = run(["corosync-cmapctl"])
773  	    if retval != 0:
774  	        return []
775  	
776  	    nodename_re = re.compile(r"^nodelist\.node\.(\d+)\.name .*= (.*)", re.M)
777  	    nodestatus_re = re.compile(
778  	        r"^runtime\.members\.(\d+).status .*= (.*)", re.M
779  	    )
780  	    nodenameid_mapping_re = re.compile(
781  	        r"nodelist\.node\.(\d+)\.nodeid .*= (\d+)", re.M
782  	    )
783  	
784  	    node_names = nodename_re.findall(output)
785  	
786  	    index_to_id = dict(nodenameid_mapping_re.findall(output))
787  	    id_to_status = dict(nodestatus_re.findall(output))
788  	
789  	    node_status = {}
790  	    for index, node_name in node_names:
791  	        if index in index_to_id:
792  	            nodeid = index_to_id[index]
793  	            if nodeid in id_to_status:
794  	                node_status[node_name] = id_to_status[nodeid]
795  	        else:
796  	            print_to_stderr(f"Error mapping {node_name}")
797  	
798  	    nodes_active = []
799  	    for node, status in node_status.items():
800  	        if status == "joined":
801  	            nodes_active.append(node)
802  	
803  	    return nodes_active
804  	
805  	
806  	# is it needed to handle corosync-qdevice service when managing cluster services
807  	def need_to_handle_qdevice_service():
808  	    """
809  	    Commandline options: no options
810  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
811  	        settings but it doesn't make sense for contexts in which this function
812  	        is used
813  	    """
814  	    try:
815  	        with open(settings.corosync_conf_file, "rb") as corosync_conf_file:
816  	            return (
817  	                corosync_conf_facade(
818  	                    corosync_conf_parser.Parser.parse(corosync_conf_file.read())
819  	                ).get_quorum_device_model()
820  	                is not None
821  	            )
822  	    except (OSError, corosync_conf_parser.CorosyncConfParserException):
823  	        # corosync.conf not present or not valid => no qdevice specified
824  	        return False
825  	
826  	
827  	# Restore default behavior before starting subprocesses
828  	def subprocess_setup():
829  	    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
830  	
831  	
832  	def touch_cib_file(cib_filename):
833  	    if not os.path.isfile(cib_filename):
834  	        try:
835  	            write_empty_cib(cib_filename)
836  	        except OSError as e:
837  	            err(
838  	                "Unable to write to file: '{0}': '{1}'".format(
839  	                    cib_filename, str(e)
840  	                )
841  	            )
842  	
843  	
844  	# Run command, with environment and return (output, retval)
845  	# DEPRECATED, please use lib.external.CommandRunner via utils.cmd_runner()
846  	def run(
847  	    args,
848  	    ignore_stderr=False,
849  	    string_for_stdin=None,
850  	    env_extend=None,
851  	    binary_output=False,
852  	):
853  	    """
854  	    Commandline options:
855  	      * -f - CIB file (effective only for some pacemaker tools)
856  	      * --debug
857  	    """
858  	    if not env_extend:
859  	        env_extend = {}
860  	    env_var = env_extend
861  	    env_var.update(dict(os.environ))
862  	    env_var["LC_ALL"] = "C"
863  	    if usefile:
864  	        env_var["CIB_file"] = filename
865  	        touch_cib_file(filename)
866  	
867  	    command = args[0]
868  	    if command[0:3] == "crm" or command in [
869  	        "cibadmin",
870  	        "stonith_admin",
871  	    ]:
872  	        args[0] = os.path.join(settings.pacemaker_execs, command)
873  	    elif command[0:8] == "corosync":
874  	        args[0] = os.path.join(settings.corosync_execs, command)
875  	
876  	    try:
877  	        if "--debug" in pcs_options:
878  	            print_to_stderr("Running: " + " ".join(args))
879  	            if string_for_stdin:
880  	                print_to_stderr(
881  	                    f"--Debug Input Start--\n"
882  	                    f"{string_for_stdin}\n"
883  	                    f"--Debug Input End--"
884  	                )
885  	
886  	        # Some commands react differently if you give them anything via stdin
887  	        if string_for_stdin is not None:
888  	            stdin_pipe = subprocess.PIPE
889  	        else:
890  	            stdin_pipe = subprocess.DEVNULL
891  	
892  	        p = subprocess.Popen(
893  	            args,
894  	            stdin=stdin_pipe,
895  	            stdout=subprocess.PIPE,
896  	            stderr=(subprocess.PIPE if ignore_stderr else subprocess.STDOUT),
897  	            preexec_fn=subprocess_setup,  # noqa: PLW1509
898  	            close_fds=True,
899  	            env=env_var,
900  	            # decodes newlines and in python3 also converts bytes to str
901  	            universal_newlines=(not binary_output),
902  	        )
903  	        output, dummy_stderror = p.communicate(string_for_stdin)
904  	        retval = p.returncode
905  	        if "--debug" in pcs_options:
906  	            print_to_stderr(
907  	                "Return Value: {retval}\n"
908  	                "--Debug Output Start--\n"
909  	                "{debug_output}\n"
910  	                "--Debug Output End--".format(
911  	                    retval=retval,
912  	                    debug_output=output.rstrip(),
913  	                )
914  	            )
915  	    except OSError as e:
916  	        print_to_stderr(e.strerror)
917  	        err("unable to locate command: " + args[0])
918  	
919  	    return output, retval
920  	
921  	
922  	def cmd_runner(cib_file_override=None):
923  	    """
924  	    Commandline options:
925  	      * -f - CIB file
926  	    """
927  	    env_vars = {}
928  	    if usefile:
929  	        env_vars["CIB_file"] = filename
930  	    if cib_file_override:
931  	        env_vars["CIB_file"] = cib_file_override
932  	    env_vars.update(os.environ)
933  	    env_vars["LC_ALL"] = "C"
934  	    return CommandRunner(
935  	        logging.getLogger("pcs"), get_report_processor(), env_vars
936  	    )
937  	
938  	
939  	def run_pcsdcli(command, data=None):
940  	    """
941  	    Commandline options:
942  	      * --request-timeout - timeout for HTTP request, applicable for commands:
943  	        * remove_known_hosts - only when running on cluster node (sync will
944  	            be initiated)
945  	        * auth
946  	        * send_local_configs
947  	    """
948  	    if not data:
949  	        data = {}
950  	    env_var = {}
951  	    if "--debug" in pcs_options:
952  	        env_var["PCSD_DEBUG"] = "true"
953  	    if "--request-timeout" in pcs_options:
954  	        env_var["PCSD_NETWORK_TIMEOUT"] = str(pcs_options["--request-timeout"])
955  	    else:
956  	        env_var["PCSD_NETWORK_TIMEOUT"] = str(settings.default_request_timeout)
957  	    pcsd_dir_path = settings.pcsd_exec_location
958  	    pcsdcli_path = os.path.join(pcsd_dir_path, "pcsd-cli.rb")
959  	    if settings.pcsd_gem_path is not None:
960  	        env_var["GEM_HOME"] = settings.pcsd_gem_path
961  	    stdout, dummy_stderr, retval = cmd_runner().run(
962  	        [settings.ruby_exec, "-I" + pcsd_dir_path, pcsdcli_path, command],
963  	        json.dumps(data),
964  	        env_var,
965  	    )
966  	    try:
967  	        output_json = json.loads(stdout)
968  	        for key in ["status", "text", "data"]:
969  	            if key not in output_json:
970  	                output_json[key] = None
971  	
972  	        output = "".join(output_json["log"])
973  	        # check if some requests timed out, if so print message about it
974  	        if "error: operation_timedout" in output:
975  	            print_to_stderr("Error: Operation timed out")
976  	        # check if there are any connection failures due to proxy in pcsd and
977  	        # print warning if so
978  	        proxy_msg = "Proxy is set in environment variables, try disabling it"
979  	        if proxy_msg in output:
980  	            reports_output.warn(proxy_msg)
981  	
982  	    except ValueError:
983  	        output_json = {
984  	            "status": "bad_json_output",
985  	            "text": stdout,
986  	            "data": None,
987  	        }
988  	    return output_json, retval
989  	
990  	
991  	def call_local_pcsd(argv, options, std_in=None):  # noqa: PLR0911
992  	    """
993  	    Commandline options:
994  	      * --request-timeout - timeout of call to local pcsd
995  	    """
996  	    # some commands cannot be run under a non-root account
997  	    # so we pass those commands to locally running pcsd to execute them
998  	    # returns [list_of_errors, exit_code, stdout, stderr]
999  	    data = {
1000 	        "command": json.dumps(argv),
1001 	        "options": json.dumps(options),
1002 	    }
1003 	    if std_in:
1004 	        data["stdin"] = std_in
1005 	    data_send = urlencode(data)
1006 	    code, output = sendHTTPRequest(
1007 	        "localhost", "run_pcs", data_send, False, False
1008 	    )
1009 	
1010 	    if code == 3:  # not authenticated
1011 	        return [
1012 	            [
1013 	                "Unable to authenticate against the local pcsd. Run the same "
1014 	                "command as root or authenticate yourself to the local pcsd "
1015 	                "using command 'pcs client local-auth'"
1016 	            ],
1017 	            1,
1018 	            "",
1019 	            "",
1020 	        ]
1021 	    if code != 0:  # http error connecting to localhost
1022 	        return [[output], 1, "", ""]
1023 	
1024 	    try:
1025 	        output_json = json.loads(output)
1026 	        for key in ["status", "data"]:
1027 	            if key not in output_json:
1028 	                output_json[key] = None
1029 	    except ValueError:
1030 	        return [["Unable to communicate with pcsd"], 1, "", ""]
1031 	    if output_json["status"] == "bad_command":
1032 	        return [["Command not allowed"], 1, "", ""]
1033 	    if output_json["status"] == "access_denied":
1034 	        return [["Access denied"], 1, "", ""]
1035 	    if output_json["status"] != "ok" or not output_json["data"]:
1036 	        return [["Unable to communicate with pcsd"], 1, "", ""]
1037 	    try:
1038 	        exitcode = output_json["data"]["code"]
1039 	        std_out = output_json["data"]["stdout"]
1040 	        std_err = output_json["data"]["stderr"]
1041 	        return [[], exitcode, std_out, std_err]
1042 	    except KeyError:
1043 	        return [["Unable to communicate with pcsd"], 1, "", ""]
1044 	
1045 	
1046 	def map_for_error_list(callab, iterab):
1047 	    """
1048 	    Commandline options: no options
1049 	    NOTE: callback 'callab' may use some options
1050 	    """
1051 	    error_list = []
1052 	    for item in iterab:
1053 	        retval, error = callab(item)
1054 	        if retval != 0:
1055 	            error_list.append(error)
1056 	    return error_list
1057 	
1058 	
1059 	def run_parallel(worker_list, wait_seconds=1):
1060 	    """
1061 	    Commandline options: no options
1062 	    """
1063 	    thread_list = set()
1064 	    for worker in worker_list:
1065 	        thread = threading.Thread(target=worker)
1066 	        thread.daemon = True
1067 	        thread.start()
1068 	        thread_list.add(thread)
1069 	
1070 	    while thread_list:
1071 	        thread = thread_list.pop()
1072 	        thread.join(wait_seconds)
1073 	        if thread.is_alive():
1074 	            thread_list.add(thread)
1075 	
1076 	
1077 	def create_task(report, action, node, *args, **kwargs):
1078 	    """
1079 	    Commandline options: no options
1080 	    """
1081 	
1082 	    def worker():
1083 	        returncode, output = action(node, *args, **kwargs)
1084 	        report(node, returncode, output)
1085 	
1086 	    return worker
1087 	
1088 	
1089 	def create_task_list(report, action, node_list, *args, **kwargs):
1090 	    """
1091 	    Commandline options: no options
1092 	    """
1093 	    return [
1094 	        create_task(report, action, node, *args, **kwargs) for node in node_list
1095 	    ]
1096 	
1097 	
1098 	def parallel_for_nodes(action, node_list, *args, **kwargs):
1099 	    """
1100 	    Commandline options: no options
1101 	    NOTE: callback 'action' may use some cmd options
1102 	    """
1103 	    node_errors = {}
1104 	
1105 	    def report(node, returncode, output):
1106 	        message = "{0}: {1}".format(node, output.strip())
1107 	        print_to_stderr(message)
1108 	        if returncode != 0:
1109 	            node_errors[node] = message
1110 	
1111 	    run_parallel(create_task_list(report, action, node_list, *args, **kwargs))
1112 	    return node_errors
1113 	
1114 	
1115 	def get_group_children(group_id):
1116 	    """
1117 	    Commandline options: no options
1118 	    """
1119 	    return dom_get_group_children(get_cib_dom(), group_id)
1120 	
1121 	
1122 	def dom_get_group_children(dom, group_id):
1123 	    groups = dom.getElementsByTagName("group")
1124 	    for g in groups:
1125 	        if g.getAttribute("id") == group_id:
1126 	            return [
1127 	                child_el.getAttribute("id")
1128 	                for child_el in get_group_children_el_from_el(g)
1129 	            ]
1130 	    return []
1131 	
1132 	
1133 	def get_group_children_el_from_el(group_el):
1134 	    child_resources = []
1135 	    for child in group_el.childNodes:
1136 	        if child.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
1137 	            continue
1138 	        if child.tagName == "primitive":
1139 	            child_resources.append(child)
1140 	    return child_resources
1141 	
1142 	
1143 	def dom_get_clone_ms_resource(dom, clone_ms_id):
1144 	    """
1145 	    Commandline options: no options
1146 	    """
1147 	    clone_ms = dom_get_clone(dom, clone_ms_id) or dom_get_master(
1148 	        dom, clone_ms_id
1149 	    )
1150 	    if clone_ms:
1151 	        return dom_elem_get_clone_ms_resource(clone_ms)
1152 	    return None
1153 	
1154 	
1155 	def dom_elem_get_clone_ms_resource(clone_ms):
1156 	    """
1157 	    Commandline options: no options
1158 	    """
1159 	    for child in clone_ms.childNodes:
1160 	        if (
1161 	            child.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1162 	            and child.tagName in ["group", "primitive"]
1163 	        ):
1164 	            return child
1165 	    return None
1166 	
1167 	
1168 	def dom_get_resource_clone_ms_parent(dom, resource_id):
1169 	    """
1170 	    Commandline options: no options
1171 	    """
1172 	    resource = dom_get_resource(dom, resource_id) or dom_get_group(
1173 	        dom, resource_id
1174 	    )
1175 	    if resource:
1176 	        return dom_get_parent_by_tag_names(resource, ["clone", "master"])
1177 	    return None
1178 	
1179 	
1180 	def dom_get_resource_bundle_parent(dom, resource_id):
1181 	    """
1182 	    Commandline options: no options
1183 	    """
1184 	    resource = dom_get_resource(dom, resource_id)
1185 	    if resource:
1186 	        return dom_get_parent_by_tag_names(resource, ["bundle"])
1187 	    return None
1188 	
1189 	
1190 	def dom_get_master(dom, master_id):
1191 	    """
1192 	    Commandline options: no options
1193 	    """
1194 	    for master in dom.getElementsByTagName("master"):
1195 	        if master.getAttribute("id") == master_id:
1196 	            return master
1197 	    return None
1198 	
1199 	
1200 	def dom_get_clone(dom, clone_id):
1201 	    """
1202 	    Commandline options: no options
1203 	    """
1204 	    for clone in dom.getElementsByTagName("clone"):
1205 	        if clone.getAttribute("id") == clone_id:
1206 	            return clone
1207 	    return None
1208 	
1209 	
1210 	def dom_get_group(dom, group_id):
1211 	    """
1212 	    Commandline options: no options
1213 	    """
1214 	    for group in dom.getElementsByTagName("group"):
1215 	        if group.getAttribute("id") == group_id:
1216 	            return group
1217 	    return None
1218 	
1219 	
1220 	def dom_get_bundle(dom, bundle_id):
1221 	    """
1222 	    Commandline options: no options
1223 	    """
1224 	    for bundle in dom.getElementsByTagName("bundle"):
1225 	        if bundle.getAttribute("id") == bundle_id:
1226 	            return bundle
1227 	    return None
1228 	
1229 	
1230 	def dom_get_resource_bundle(bundle_el):
1231 	    """
1232 	    Commandline options: no options
1233 	    """
1234 	    for child in bundle_el.childNodes:
1235 	        if (
1236 	            child.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1237 	            and child.tagName == "primitive"
1238 	        ):
1239 	            return child
1240 	    return None
1241 	
1242 	
1243 	def dom_get_group_clone(dom, group_id):
1244 	    """
1245 	    Commandline options: no options
1246 	    """
1247 	    for clone in dom.getElementsByTagName("clone"):
1248 	        group = dom_get_group(clone, group_id)
1249 	        if group:
1250 	            return group
1251 	    return None
1252 	
1253 	
1254 	def dom_get_group_masterslave(dom, group_id):
1255 	    """
1256 	    Commandline options: no options
1257 	    """
1258 	    for master in dom.getElementsByTagName("master"):
1259 	        group = dom_get_group(master, group_id)
1260 	        if group:
1261 	            return group
1262 	    return None
1263 	
1264 	
1265 	def dom_get_resource(dom, resource_id):
1266 	    """
1267 	    Commandline options: no options
1268 	    """
1269 	    for primitive in dom.getElementsByTagName("primitive"):
1270 	        if primitive.getAttribute("id") == resource_id:
1271 	            return primitive
1272 	    return None
1273 	
1274 	
1275 	def dom_get_any_resource(dom, resource_id):
1276 	    """
1277 	    Commandline options: no options
1278 	    """
1279 	    return (
1280 	        dom_get_resource(dom, resource_id)
1281 	        or dom_get_group(dom, resource_id)
1282 	        or dom_get_clone(dom, resource_id)
1283 	        or dom_get_master(dom, resource_id)
1284 	    )
1285 	
1286 	
1287 	def dom_get_resource_clone(dom, resource_id):
1288 	    """
1289 	    Commandline options: no options
1290 	    """
1291 	    for clone in dom.getElementsByTagName("clone"):
1292 	        resource = dom_get_resource(clone, resource_id)
1293 	        if resource:
1294 	            return resource
1295 	    return None
1296 	
1297 	
1298 	def dom_get_resource_masterslave(dom, resource_id):
1299 	    """
1300 	    Commandline options: no options
1301 	    """
1302 	    for master in dom.getElementsByTagName("master"):
1303 	        resource = dom_get_resource(master, resource_id)
1304 	        if resource:
1305 	            return resource
1306 	    return None
1307 	
1308 	
1309 	# returns tuple (is_valid, error_message, correct_resource_id_if_exists)
1310 	# there is a duplicate code in pcs/lib/cib/constraint/constraint.py
1311 	# please use function in pcs/lib/cib/constraint/constraint.py
1312 	def validate_constraint_resource(dom, resource_id):  # noqa: PLR0911
1313 	    """
1314 	    Commandline options:
1315 	      * --force - allow constraint on any resource
1316 	    """
1317 	    resource_el = (
1318 	        dom_get_clone(dom, resource_id)
1319 	        or dom_get_master(dom, resource_id)
1320 	        or dom_get_bundle(dom, resource_id)
1321 	    )
1322 	    if resource_el:
1323 	        # clones, masters and bundles are always valid
1324 	        return True, "", resource_id
1325 	
1326 	    resource_el = dom_get_resource(dom, resource_id) or dom_get_group(
1327 	        dom, resource_id
1328 	    )
1329 	    if not resource_el:
1330 	        return False, "Resource '%s' does not exist" % resource_id, None
1331 	
1332 	    clone_el = dom_get_resource_clone_ms_parent(
1333 	        dom, resource_id
1334 	    ) or dom_get_resource_bundle_parent(dom, resource_id)
1335 	    if not clone_el:
1336 	        # a primitive and a group is valid if not in a clone nor a master nor a
1337 	        # bundle
1338 	        return True, "", resource_id
1339 	
1340 	    if "--force" in pcs_options:
1341 	        return True, "", clone_el.getAttribute("id")
1342 	
1343 	    if clone_el.tagName in ["clone", "master"]:
1344 	        return (
1345 	            False,
1346 	            "%s is a clone resource, you should use the clone id: %s "
1347 	            "when adding constraints. Use --force to override."
1348 	            % (resource_id, clone_el.getAttribute("id")),
1349 	            clone_el.getAttribute("id"),
1350 	        )
1351 	    if clone_el.tagName == "bundle":
1352 	        return (
1353 	            False,
1354 	            "%s is a bundle resource, you should use the bundle id: %s "
1355 	            "when adding constraints. Use --force to override."
1356 	            % (resource_id, clone_el.getAttribute("id")),
1357 	            clone_el.getAttribute("id"),
1358 	        )
1359 	    return True, "", resource_id
1360 	
1361 	
1362 	def validate_resources_not_in_same_group(dom, resource_id1, resource_id2):
1363 	    resource_el1 = dom_get_resource(dom, resource_id1)
1364 	    resource_el2 = dom_get_resource(dom, resource_id2)
1365 	    if not resource_el1 or not resource_el2:
1366 	        # Only primitive resources can be in a group. If at least one of the
1367 	        # resources is not a primitive (resource_el is None), then the
1368 	        # resources are not in the same group.
1369 	        return True
1370 	    group1 = dom_get_parent_by_tag_names(resource_el1, ["group"])
1371 	    group2 = dom_get_parent_by_tag_names(resource_el2, ["group"])
1372 	    if not group1 or not group2:
1373 	        return True
1374 	    return group1 != group2
1375 	
1376 	
1377 	def dom_get_resource_remote_node_name(dom_resource):
1378 	    """
1379 	    Commandline options: no options
1380 	    """
1381 	    if dom_resource.tagName != "primitive":
1382 	        return None
1383 	    if (
1384 	        dom_resource.getAttribute("class").lower() == "ocf"
1385 	        and dom_resource.getAttribute("provider").lower() == "pacemaker"
1386 	        and dom_resource.getAttribute("type").lower() == "remote"
1387 	    ):
1388 	        return dom_resource.getAttribute("id")
1389 	    return dom_get_meta_attr_value(dom_resource, "remote-node")
1390 	
1391 	
1392 	def dom_get_meta_attr_value(dom_resource, meta_name):
1393 	    """
1394 	    Commandline options: no options
1395 	    """
1396 	    for meta in dom_resource.getElementsByTagName("meta_attributes"):
1397 	        for nvpair in meta.getElementsByTagName("nvpair"):
1398 	            if nvpair.getAttribute("name") == meta_name:
1399 	                return nvpair.getAttribute("value")
1400 	    return None
1401 	
1402 	
1403 	def dom_get_node(dom, node_name):
1404 	    """
1405 	    Commandline options: no options
1406 	    """
1407 	    for e in dom.getElementsByTagName("node"):
1408 	        if e.hasAttribute("uname") and e.getAttribute("uname") == node_name:
1409 	            return e
1410 	    return None
1411 	
1412 	
1413 	def _dom_get_children_by_tag_name(dom_el, tag_name):
1414 	    """
1415 	    Commandline options: no options
1416 	    """
1417 	    return [
1418 	        node
1419 	        for node in dom_el.childNodes
1420 	        if node.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1421 	        and node.tagName == tag_name
1422 	    ]
1423 	
1424 	
1425 	def dom_get_parent_by_tag_names(dom_el, tag_names):
1426 	    """
1427 	    Commandline options: no options
1428 	    """
1429 	    parent = dom_el.parentNode
1430 	    while parent:
1431 	        if not isinstance(parent, xml.dom.minidom.Element):
1432 	            return None
1433 	        if parent.tagName in tag_names:
1434 	            return parent
1435 	        parent = parent.parentNode
1436 	    return None
1437 	
1438 	
1439 	# moved to pcs.lib.pacemaker.state
1440 	def get_resource_for_running_check(cluster_state, resource_id, stopped=False):
1441 	    """
1442 	    Commandline options: no options
1443 	    """
1444 	
1445 	    def _isnum(value):
1446 	        return all(char in list("0123456789") for char in value)
1447 	
1448 	    for clone in cluster_state.getElementsByTagName("clone"):
1449 	        if clone.getAttribute("id") == resource_id:
1450 	            for child in clone.childNodes:
1451 	                if child.nodeType == child.ELEMENT_NODE and child.tagName in [
1452 	                    "resource",
1453 	                    "group",
1454 	                ]:
1455 	                    resource_id = child.getAttribute("id")
1456 	                    # in a clone, a resource can have an id of '<name>:N'
1457 	                    if ":" in resource_id:
1458 	                        parts = resource_id.rsplit(":", 1)
1459 	                        if _isnum(parts[1]):
1460 	                            resource_id = parts[0]
1461 	                    break
1462 	    for group in cluster_state.getElementsByTagName("group"):
1463 	        # If resource is a clone it can have an id of '<resource name>:N'
1464 	        if group.getAttribute("id") == resource_id or group.getAttribute(
1465 	            "id"
1466 	        ).startswith(resource_id + ":"):
1467 	            if stopped:
1468 	                elem = group.getElementsByTagName("resource")[0]
1469 	            else:
1470 	                elem = group.getElementsByTagName("resource")[-1]
1471 	            resource_id = elem.getAttribute("id")
1472 	    return resource_id
1473 	
1474 	
1475 	# moved to pcs.lib.pacemaker.state
1476 	# see pcs.lib.commands.resource for usage
1477 	def resource_running_on(resource, passed_state=None, stopped=False):
1478 	    """
1479 	    Commandline options:
1480 	      * -f - has effect but doesn't make sense to check state of resource
1481 	    """
1482 	    nodes_started = []
1483 	    nodes_promoted = []
1484 	    nodes_unpromoted = []
1485 	    state = passed_state if passed_state else getClusterState()
1486 	    resource_original = resource
1487 	    resource = get_resource_for_running_check(state, resource, stopped)
1488 	    resources = state.getElementsByTagName("resource")
1489 	    for res in resources:
1490 	        # If resource is a clone it can have an id of '<resource name>:N'
1491 	        # If resource is a clone it will be found more than once - cannot break
1492 	        if (
1493 	            res.getAttribute("id") == resource
1494 	            or res.getAttribute("id").startswith(resource + ":")
1495 	        ) and res.getAttribute("failed") != "true":
1496 	            for node in res.getElementsByTagName("node"):
1497 	                node_name = node.getAttribute("name")
1498 	                role = res.getAttribute("role")
1499 	                if role == const.PCMK_ROLE_STARTED:
1500 	                    nodes_started.append(node_name)
1501 	                elif role in (
1502 	                    const.PCMK_ROLE_PROMOTED,
1503 	                    const.PCMK_ROLE_PROMOTED_LEGACY,
1504 	                ):
1505 	                    nodes_promoted.append(node_name)
1506 	                elif role in (
1507 	                    const.PCMK_ROLE_UNPROMOTED,
1508 	                    const.PCMK_ROLE_UNPROMOTED_LEGACY,
1509 	                ):
1510 	                    nodes_unpromoted.append(node_name)
1511 	    if not nodes_started and not nodes_promoted and not nodes_unpromoted:
1512 	        message = "Resource '%s' is not running on any node" % resource_original
1513 	    else:
1514 	        message_parts = []
1515 	        for alist, label in (
1516 	            (nodes_started, "running"),
1517 	            (nodes_promoted, str(const.PCMK_ROLE_PROMOTED).lower()),
1518 	            (nodes_unpromoted, str(const.PCMK_ROLE_UNPROMOTED).lower()),
1519 	        ):
1520 	            if alist:
1521 	                alist.sort()
1522 	                message_parts.append(
1523 	                    "%s on node%s %s"
1524 	                    % (label, "s" if len(alist) > 1 else "", ", ".join(alist))
1525 	                )
1526 	        message = "Resource '%s' is %s." % (
1527 	            resource_original,
1528 	            "; ".join(message_parts),
1529 	        )
1530 	    return {
1531 	        "message": message,
1532 	        "is_running": bool(nodes_started or nodes_promoted or nodes_unpromoted),
1533 	    }
1534 	
1535 	
1536 	def validate_wait_get_timeout(need_cib_support=True):
1537 	    """
1538 	    Commandline options:
1539 	      * --wait
1540 	      * -f - to check if -f and --wait are not used simultaneously
1541 	    """
1542 	    if need_cib_support and usefile:
1543 	        err("Cannot use '-f' together with '--wait'")
1544 	    wait_timeout = pcs_options["--wait"]
1545 	    if wait_timeout is None:
1546 	        return wait_timeout
1547 	    wait_timeout = timeout_to_seconds(wait_timeout)
1548 	    if wait_timeout is None:
1549 	        err(
1550 	            "%s is not a valid number of seconds to wait"
1551 	            % pcs_options["--wait"]
1552 	        )
1553 	    return wait_timeout
1554 	
1555 	
1556 	# Return matches from the CIB with the xpath_query
1557 	def get_cib_xpath(xpath_query):
1558 	    """
1559 	    Commandline options:
1560 	      * -f - CIB file
1561 	    """
1562 	    args = ["cibadmin", "-Q", "--xpath", xpath_query]
1563 	    output, retval = run(args)
1564 	    if retval != 0:
1565 	        return ""
1566 	    return output
1567 	
1568 	
1569 	def get_cib(scope=None):
1570 	    """
1571 	    Commandline options:
1572 	      * -f - CIB file
1573 	    """
1574 	    command = ["cibadmin", "-l", "-Q"]
1575 	    if scope:
1576 	        command.append("--scope=%s" % scope)
1577 	    output, retval = run(command)
1578 	    if retval != 0:
1579 	        if retval == 105 and scope:
1580 	            err("unable to get cib, scope '%s' not present in cib" % scope)
1581 	        else:
1582 	            err("unable to get cib")
1583 	    return output
1584 	
1585 	
1586 	def get_cib_dom(cib_xml=None):
1587 	    """
1588 	    Commandline options:
1589 	      * -f - CIB file
1590 	    """
1591 	    if cib_xml is None:
1592 	        cib_xml = get_cib()
1593 	    try:
1594 	        return parseString(cib_xml)
1595 	    except xml.parsers.expat.ExpatError:
1596 	        return err("unable to get cib")
1597 	
1598 	
1599 	# Replace only configuration section of cib with dom passed
1600 	def replace_cib_configuration(dom):
1601 	    """
1602 	    Commandline options:
1603 	      * -f - CIB file
1604 	    """
1605 	    new_dom = dom.toxml() if hasattr(dom, "toxml") else dom
1606 	    cmd = ["cibadmin", "--replace", "-V", "--xml-pipe", "-o", "configuration"]
1607 	    output, retval = run(cmd, False, new_dom)
1608 	    if retval != 0:
1609 	        err("Unable to update cib\n" + output)
1610 	
1611 	
1612 	def is_valid_cib_scope(scope):
1613 	    """
1614 	    Commandline options: no options
1615 	    """
1616 	    return scope in [
1617 	        "acls",
1618 	        "alerts",
1619 	        "configuration",
1620 	        "constraints",
1621 	        "crm_config",
1622 	        "fencing-topology",
1623 	        "nodes",
1624 	        "op_defaults",
1625 	        "resources",
1626 	        "rsc_defaults",
1627 	        "tags",
1628 	    ]
1629 	
1630 	
1631 	# Checks to see if id exists in the xml dom passed
1632 	# DEPRECATED use lxml version available in pcs.lib.cib.tools
1633 	def does_id_exist(dom, check_id):
1634 	    """
1635 	    Commandline options: no options
1636 	    """
1637 	    # do not search in /cib/status, it may contain references to previously
1638 	    # existing and deleted resources and thus preventing creating them again
1639 	    document = (
1640 	        dom if isinstance(dom, xml.dom.minidom.Document) else dom.ownerDocument
1641 	    )
1642 	    cib_found = False
1643 	    for cib in _dom_get_children_by_tag_name(document, "cib"):
1644 	        cib_found = True
1645 	        for section in cib.childNodes:
1646 	            if section.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
1647 	                continue
1648 	            if section.tagName == "status":
1649 	                continue
1650 	            for elem in section.getElementsByTagName("*"):
1651 	                if elem.getAttribute("id") == check_id:
1652 	                    return True
1653 	    if not cib_found:
1654 	        for elem in document.getElementsByTagName("*"):
1655 	            if elem.getAttribute("id") == check_id:
1656 	                return True
1657 	    return False
1658 	
1659 	
1660 	# Returns check_id if it doesn't exist in the dom, otherwise it adds an integer
1661 	# to the end of the id and increments it until a unique id is found
1662 	# DEPRECATED use lxml version available in pcs.lib.cib.tools
1663 	def find_unique_id(dom, check_id):
1664 	    """
1665 	    Commandline options: no options
1666 	    """
1667 	    counter = 1
1668 	    temp_id = check_id
1669 	    while does_id_exist(dom, temp_id):
1670 	        temp_id = check_id + "-" + str(counter)
1671 	        counter += 1
1672 	    return temp_id
1673 	
1674 	
1675 	# Checks to see if the specified operation already exists in passed set of
1676 	# operations
1677 	# pacemaker differentiates between operations only by name and interval
1678 	def operation_exists(operations_el, op_el):
1679 	    """
1680 	    Commandline options: no options
1681 	    """
1682 	    op_name = op_el.getAttribute("name")
1683 	    op_interval = timeout_to_seconds_legacy(op_el.getAttribute("interval"))
1684 	    return [
1685 	        op
1686 	        for op in operations_el.getElementsByTagName("op")
1687 	        if (
1688 	            op.getAttribute("name") == op_name
1689 	            and timeout_to_seconds_legacy(op.getAttribute("interval"))
1690 	            == op_interval
1691 	        )
1692 	    ]
1693 	
1694 	
1695 	def operation_exists_by_name(operations_el, op_el):
1696 	    """
1697 	    Commandline options: no options
1698 	    """
1699 	
1700 	    def get_role(_el, new_roles_supported):
1701 	        return common_pacemaker.role.get_value_for_cib(
1702 	            _el.getAttribute("role") or const.PCMK_ROLE_STARTED,
1703 	            new_roles_supported,
1704 	        )
1705 	
1706 	    new_roles_supported = isCibVersionSatisfied(
1707 	        operations_el, const.PCMK_NEW_ROLES_CIB_VERSION
1708 	    )
1709 	    existing = []
1710 	    op_name = op_el.getAttribute("name")
1711 	    op_role = get_role(op_el, new_roles_supported)
1712 	    ocf_check_level = None
1713 	    if op_name == "monitor":
1714 	        ocf_check_level = get_operation_ocf_check_level(op_el)
1715 	
1716 	    for op in operations_el.getElementsByTagName("op"):
1717 	        if op.getAttribute("name") == op_name:
1718 	            if (
1719 	                op_name != "monitor"
1720 	                or get_role(op, new_roles_supported) == op_role
1721 	                and ocf_check_level == get_operation_ocf_check_level(op)
1722 	            ):
1723 	                existing.append(op)
1724 	    return existing
1725 	
1726 	
1727 	def get_operation_ocf_check_level(operation_el):
1728 	    """
1729 	    Commandline options: no options
1730 	    """
1731 	    for attr_el in operation_el.getElementsByTagName("instance_attributes"):
1732 	        for nvpair_el in attr_el.getElementsByTagName("nvpair"):
1733 	            if (
1734 	                nvpair_el.getAttribute("name")
1735 	                == OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME
1736 	            ):
1737 	                return nvpair_el.getAttribute("value")
1738 	    return None
1739 	
1740 	
1741 	def set_node_attribute(prop, value, node):
1742 	    """
1743 	    Commandline options:
1744 	      * -f - CIB file
1745 	      * --force - no error if attribute to delete doesn't exist
1746 	    """
1747 	    if value == "":
1748 	        o, r = run(
1749 	            [
1750 	                "crm_attribute",
1751 	                "-t",
1752 	                "nodes",
1753 	                "--node",
1754 	                node,
1755 	                "--name",
1756 	                prop,
1757 	                "--query",
1758 	            ]
1759 	        )
1760 	        if r != 0 and "--force" not in pcs_options:
1761 	            err(
1762 	                "attribute: '%s' doesn't exist for node: '%s'" % (prop, node),
1763 	                False,
1764 	            )
1765 	            # This return code is used by pcsd
1766 	            sys.exit(2)
1767 	        o, r = run(
1768 	            [
1769 	                "crm_attribute",
1770 	                "-t",
1771 	                "nodes",
1772 	                "--node",
1773 	                node,
1774 	                "--name",
1775 	                prop,
1776 	                "--delete",
1777 	            ]
1778 	        )
1779 	    else:
1780 	        o, r = run(
1781 	            [
1782 	                "crm_attribute",
1783 	                "-t",
1784 	                "nodes",
1785 	                "--node",
1786 	                node,
1787 	                "--name",
1788 	                prop,
1789 	                "--update",
1790 	                value,
1791 	            ]
1792 	        )
1793 	
1794 	    if r != 0:
1795 	        err("unable to set attribute %s\n%s" % (prop, o))
1796 	
1797 	
1798 	def get_terminal_input(message=None):
1799 	    """
1800 	    Commandline options: no options
1801 	    """
1802 	    if message:
1803 	        sys.stdout.write(message)
1804 	        sys.stdout.flush()
1805 	    try:
1806 	        return input("")
1807 	    except EOFError:
1808 	        return ""
1809 	    except KeyboardInterrupt:
1810 	        print("Interrupted")
1811 	        sys.exit(1)
1812 	
1813 	
1814 	def _get_continue_confirmation_interactive(warning_text: str) -> bool:
1815 	    """
1816 	    Warns user and asks for permission to continue. Returns True if user wishes
1817 	    to continue, False otherwise.
1818 	
1819 	    This function is mostly intended for prompting user to confirm destructive
1820 	    operations - that's why WARNING is in all caps here and user is asked to
1821 	    explicitly type 'yes' or 'y' to continue.
1822 	
1823 	    warning_text -- describes action that we want the user to confirm
1824 	    """
1825 	    print(f"WARNING: {warning_text}")
1826 	    print("Type 'yes' or 'y' to proceed, anything else to cancel: ", end="")
1827 	    response = get_terminal_input()
1828 	    if response in ["yes", "y"]:
1829 	        return True
1830 	    print("Canceled")
1831 	    return False
1832 	
1833 	
1834 	def is_run_interactive() -> bool:
1835 	    """
1836 	    Return True if pcs is running in an interactive environment, False otherwise
1837 	    """
1838 	    return (
1839 	        sys.stdin is not None
1840 	        and sys.stdout is not None
1841 	        and sys.stdin.isatty()
1842 	        and sys.stdout.isatty()
1843 	    )
1844 	
1845 	
1846 	def get_continue_confirmation(warning_text: str, yes: bool) -> bool:
1847 	    """
1848 	    Either asks user to confirm continuation interactively or use --yes to
1849 	    override when running from a script. Returns True if user wants to continue.
1850 	    Returns False if user cancels the action. If a non-interactive environment
1851 	    is detected, pcs exits with an error formed from warning_text.
1852 	
1853 	    warning_text -- describes action that we want the user to confirm
1854 	    yes -- was --yes flag provided?
1855 	    """
1856 	    if yes:
1857 	        reports_output.warn(warning_text)
1858 	        return True
1859 	    if not is_run_interactive():
1860 	        err(f"{warning_text}, use --yes to override")
1861 	        return False
1862 	    return _get_continue_confirmation_interactive(warning_text)
1863 	
1864 	
1865 	def get_terminal_password(message="Password: "):
1866 	    """
1867 	    Commandline options: no options
1868 	    """
1869 	    if sys.stdin is not None and sys.stdin.isatty():
1870 	        try:
1871 	            return getpass.getpass(message)
1872 	        except KeyboardInterrupt:
1873 	            print("Interrupted")
1874 	            sys.exit(1)
1875 	    else:
1876 	        return get_terminal_input(message)
1877 	
1878 	
1879 	# Returns an xml dom containing the current status of the cluster
1880 	# DEPRECATED, please use
1881 	# ClusterState(lib.pacemaker.live.get_cluster_status_dom()) instead
1882 	def getClusterState():
1883 	    """
1884 	    Commandline options:
1885 	      * -f - CIB file
1886 	    """
1887 	    xml_string, returncode = run(
1888 	        ["crm_mon", "--one-shot", "--output-as=xml", "--inactive"],
1889 	        ignore_stderr=True,
1890 	    )
1891 	    if returncode != 0:
1892 	        err("error running crm_mon, is pacemaker running?")
1893 	    return parseString(xml_string)
1894 	
1895 	
1896 	def write_empty_cib(cibfile):
1897 	    """
1898 	    Commandline options: no options
1899 	    """
1900 	    empty_xml = """
1901 	        <cib admin_epoch="0" epoch="1" num_updates="1" validate-with="pacemaker-3.1">
1902 	          <configuration>
1903 	            <crm_config/>
1904 	            <nodes/>
1905 	            <resources/>
1906 	            <constraints/>
1907 	          </configuration>
1908 	          <status/>
1909 	        </cib>
1910 	    """
1911 	    with open(cibfile, "w") as f:
1912 	        f.write(empty_xml)
1913 	
1914 	
1915 	def validate_xml_id(var: str, description: str = "id") -> tuple[bool, str]:
1916 	    """
1917 	    Commandline options: no options
1918 	    """
1919 	    report_list: ReportItemList = []
1920 	    validate_id(var, description, report_list)
1921 	    if report_list:
1922 	        return False, report_list[0].message.message
1923 	    return True, ""
1924 	
1925 	
1926 	def err(errorText: str, exit_after_error: bool = True) -> None:
1927 	    retval = reports_output.error(errorText)
1928 	    if exit_after_error:
1929 	        raise retval
1930 	
1931 	
1932 	@lru_cache(typed=True)
1933 	def get_service_manager() -> ServiceManagerInterface:
1934 	    return _get_service_manager(cmd_runner(), get_report_processor())
1935 	
1936 	
1937 	def enableServices():
1938 	    """
1939 	    Commandline options: no options
1940 	    """
1941 	    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
1942 	    service_list = ["corosync", "pacemaker"]
1943 	    if need_to_handle_qdevice_service():
1944 	        service_list.append("corosync-qdevice")
1945 	    service_manager = get_service_manager()
1946 	
1947 	    report_item_list = []
1948 	    for service in service_list:
1949 	        try:
1950 	            service_manager.enable(service)
1951 	        except ManageServiceError as e:
1952 	            report_item_list.append(service_exception_to_report(e))
1953 	    if report_item_list:
1954 	        raise LibraryError(*report_item_list)
1955 	
1956 	
1957 	def disableServices():
1958 	    """
1959 	    Commandline options: no options
1960 	    """
1961 	    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
1962 	    service_list = ["corosync", "pacemaker"]
1963 	    if need_to_handle_qdevice_service():
1964 	        service_list.append("corosync-qdevice")
1965 	    service_manager = get_service_manager()
1966 	
1967 	    report_item_list = []
1968 	    for service in service_list:
1969 	        try:
1970 	            service_manager.disable(service)
1971 	        except ManageServiceError as e:
1972 	            report_item_list.append(service_exception_to_report(e))
1973 	    if report_item_list:
1974 	        raise LibraryError(*report_item_list)
1975 	
1976 	
1977 	def start_service(service):
1978 	    """
1979 	    Commandline options: no options
1980 	    """
1981 	    service_manager = get_service_manager()
1982 	
1983 	    try:
1984 	        service_manager.start(service)
1985 	    except ManageServiceError as e:
1986 	        raise LibraryError(service_exception_to_report(e)) from e
1987 	
1988 	
1989 	def stop_service(service):
1990 	    """
1991 	    Commandline options: no options
1992 	    """
1993 	    service_manager = get_service_manager()
1994 	
1995 	    try:
1996 	        service_manager.stop(service)
1997 	    except ManageServiceError as e:
1998 	        raise LibraryError(service_exception_to_report(e)) from e
1999 	
2000 	
2001 	def write_file(path, data, permissions=0o644, binary=False):
2002 	    """
2003 	    Commandline options:
2004 	      * --force - overwrite a file if it already exists
2005 	    """
2006 	    if os.path.exists(path):
2007 	        if "--force" not in pcs_options:
2008 	            return False, "'%s' already exists, use --force to overwrite" % path
2009 	        try:
2010 	            os.remove(path)
2011 	        except OSError as e:
2012 	            return False, "unable to remove '%s': %s" % (path, e)
2013 	    mode = "wb" if binary else "w"
2014 	    try:
2015 	        with os.fdopen(
2016 	            os.open(path, os.O_WRONLY | os.O_CREAT, permissions), mode
2017 	        ) as outfile:
2018 	            outfile.write(data)
2019 	    except OSError as e:
2020 	        return False, "unable to write to '%s': %s" % (path, e)
2021 	    return True, ""
2022 	
2023 	
2024 	def tar_add_file_data(  # noqa: PLR0913
2025 	    tarball,
2026 	    data,
2027 	    name,
2028 	    *,
2029 	    mode=None,
2030 	    uid=None,
2031 	    gid=None,
2032 	    uname=None,
2033 	    gname=None,
2034 	    mtime=None,
2035 	):
2036 	    """
2037 	    Commandline options: no options
2038 	    """
2039 	    info = tarfile.TarInfo(name)
2040 	    info.size = len(data)
2041 	    info.type = tarfile.REGTYPE
2042 	    info.mtime = int(time.time()) if mtime is None else mtime
2043 	    if mode is not None:
2044 	        info.mode = mode
2045 	    if uid is not None:
2046 	        info.uid = uid
2047 	    if gid is not None:
2048 	        info.gid = gid
2049 	    if uname is not None:
2050 	        info.uname = uname
2051 	    if gname is not None:
2052 	        info.gname = gname
2053 	    data_io = BytesIO(data)
2054 	    tarball.addfile(info, data_io)
2055 	    data_io.close()
2056 	
2057 	
2058 	# DEPRECATED, please use pcs.lib.pacemaker.live.simulate_cib
2059 	def simulate_cib(cib_dom):
2060 	    """
2061 	    Commandline options: no options
2062 	    """
2063 	    try:
2064 	        with (
2065 	            tempfile.NamedTemporaryFile(
2066 	                mode="w+", suffix=".pcs"
2067 	            ) as new_cib_file,
2068 	            tempfile.NamedTemporaryFile(
2069 	                mode="w+", suffix=".pcs"
2070 	            ) as transitions_file,
2071 	        ):
2072 	            output, retval = run(
2073 	                [
2074 	                    "crm_simulate",
2075 	                    "--simulate",
2076 	                    "--save-output",
2077 	                    new_cib_file.name,
2078 	                    "--save-graph",
2079 	                    transitions_file.name,
2080 	                    "--xml-pipe",
2081 	                ],
2082 	                string_for_stdin=cib_dom.toxml(),
2083 	            )
2084 	            if retval != 0:
2085 	                return err("Unable to run crm_simulate:\n%s" % output)
2086 	            new_cib_file.seek(0)
2087 	            transitions_file.seek(0)
2088 	            return (
2089 	                output,
2090 	                parseString(transitions_file.read()),
CID (unavailable; MK=c30004330dc7a1580afdb00f63baac8c) (#2 of 2): 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.
2091 	                parseString(new_cib_file.read()),
2092 	            )
2093 	    except (OSError, xml.parsers.expat.ExpatError) as e:
2094 	        return err("Unable to run crm_simulate:\n%s" % e)
2095 	    except xml.etree.ElementTree.ParseError as e:
2096 	        return err("Unable to run crm_simulate:\n%s" % e)
2097 	
2098 	
2099 	# DEPRECATED
2100 	# please use pcs.lib.pacemaker.simulate.get_operations_from_transitions
2101 	def get_operations_from_transitions(transitions_dom):
2102 	    """
2103 	    Commandline options: no options
2104 	    """
2105 	    operation_list = []
2106 	    watched_operations = (
2107 	        "start",
2108 	        "stop",
2109 	        "promote",
2110 	        "demote",
2111 	        "migrate_from",
2112 	        "migrate_to",
2113 	    )
2114 	    for rsc_op in transitions_dom.getElementsByTagName("rsc_op"):
2115 	        primitives = rsc_op.getElementsByTagName("primitive")
2116 	        if not primitives:
2117 	            continue
2118 	        if rsc_op.getAttribute("operation").lower() not in watched_operations:
2119 	            continue
2120 	        for prim in primitives:
2121 	            prim_id = prim.getAttribute("id")
2122 	            operation_list.append(
2123 	                (
2124 	                    int(rsc_op.getAttribute("id")),
2125 	                    {
2126 	                        "id": prim_id,
2127 	                        "long_id": prim.getAttribute("long-id") or prim_id,
2128 	                        "operation": rsc_op.getAttribute("operation").lower(),
2129 	                        "on_node": rsc_op.getAttribute("on_node"),
2130 	                    },
2131 	                )
2132 	            )
2133 	    operation_list.sort(key=lambda x: x[0])
2134 	    return [op[1] for op in operation_list]
2135 	
2136 	
2137 	def get_resources_location_from_operations(cib_dom, resources_operations):
2138 	    """
2139 	    Commandline options:
2140 	      * --force - allow constraints on any resource, may not have any effect as
2141 	        an invalid constraint is ignored anyway
2142 	    """
2143 	    locations = {}
2144 	    for res_op in resources_operations:
2145 	        operation = res_op["operation"]
2146 	        if operation not in ("start", "promote", "migrate_from"):
2147 	            continue
2148 	        long_id = res_op["long_id"]
2149 	        if long_id not in locations:
2150 	            # Move clone instances as if they were non-cloned resources, it
2151 	            # really works with current pacemaker (1.1.13-6). Otherwise there
2152 	            # is probably no way to move them other then setting their
2153 	            # stickiness to 0.
2154 	            res_id = res_op["id"]
2155 	            if ":" in res_id:
2156 	                res_id = res_id.split(":")[0]
2157 	            id_for_constraint = validate_constraint_resource(cib_dom, res_id)[2]
2158 	            if not id_for_constraint:
2159 	                continue
2160 	            locations[long_id] = {
2161 	                "id": res_op["id"],
2162 	                "long_id": long_id,
2163 	                "id_for_constraint": id_for_constraint,
2164 	            }
2165 	        if operation in ("start", "migrate_from"):
2166 	            locations[long_id]["start_on_node"] = res_op["on_node"]
2167 	        if operation == "promote":
2168 	            locations[long_id]["promote_on_node"] = res_op["on_node"]
2169 	    return {
2170 	        key: val
2171 	        for key, val in locations.items()
2172 	        if "start_on_node" in val or "promote_on_node" in val
2173 	    }
2174 	
2175 	
2176 	def get_remote_quorumtool_output(node):
2177 	    """
2178 	    Commandline options:
2179 	      * --request-timeout - timeout for HTTP requests
2180 	    """
2181 	    return sendHTTPRequest(node, "remote/get_quorum_info", None, False, False)
2182 	
2183 	
2184 	# return True if quorumtool_output is a string returned when the node is off
2185 	def is_node_offline_by_quorumtool_output(quorum_info):
2186 	    """
2187 	    Commandline options: no options
2188 	    """
2189 	    return quorum_info.strip() == "Cannot initialize CMAP service"
2190 	
2191 	
2192 	def dom_prepare_child_element(dom_element, tag_name, id_candidate):
2193 	    """
2194 	    Commandline options: no options
2195 	    """
2196 	    child_elements = [
2197 	        child
2198 	        for child in dom_element.childNodes
2199 	        if child.nodeType == child.ELEMENT_NODE and child.tagName == tag_name
2200 	    ]
2201 	
2202 	    if not child_elements:
2203 	        dom = dom_element.ownerDocument
2204 	        child_element = dom.createElement(tag_name)
2205 	        child_element.setAttribute("id", find_unique_id(dom, id_candidate))
2206 	        dom_element.appendChild(child_element)
2207 	    else:
2208 	        child_element = child_elements[0]
2209 	    return child_element
2210 	
2211 	
2212 	def dom_update_nvset(dom_element, nvpair_tuples, tag_name, id_candidate):
2213 	    """
2214 	    Commandline options: no options
2215 	    """
2216 	    # Already ported to pcs.libcib.nvpair
2217 	
2218 	    # Do not ever remove the nvset element, even if it is empty. There may be
2219 	    # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
2220 	    # and removing) but not nvsets. In such a case, removing the nvset would
2221 	    # cause the whole change to be rejected by pacemaker with a "permission
2222 	    # denied" message.
2223 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
2224 	    if not nvpair_tuples:
2225 	        return
2226 	
2227 	    only_removing = True
2228 	    for _, value in nvpair_tuples:
2229 	        if value != "":
2230 	            only_removing = False
2231 	            break
2232 	
2233 	    # Do not use dom.getElementsByTagName, that would get elements we do not
2234 	    # want to. For example if dom_element is a clone, we would get the clones's
2235 	    # as well as clone's primitive's attributes.
2236 	    nvset_element_list = _dom_get_children_by_tag_name(dom_element, tag_name)
2237 	
2238 	    # Do not create new nvset if we are only removing values from it.
2239 	    if not nvset_element_list and only_removing:
2240 	        return
2241 	
2242 	    if not nvset_element_list:
2243 	        dom = dom_element.ownerDocument
2244 	        nvset_element = dom.createElement(tag_name)
2245 	        nvset_element.setAttribute("id", find_unique_id(dom, id_candidate))
2246 	        dom_element.appendChild(nvset_element)
2247 	    else:
2248 	        nvset_element = nvset_element_list[0]
2249 	
2250 	    for name, value in nvpair_tuples:
2251 	        dom_update_nv_pair(
2252 	            nvset_element, name, value, nvset_element.getAttribute("id") + "-"
2253 	        )
2254 	
2255 	
2256 	def dom_update_nv_pair(dom_element, name, value, id_prefix=""):
2257 	    """
2258 	    Commandline options: no options
2259 	    """
2260 	    # Do not ever remove the nvset element, even if it is empty. There may be
2261 	    # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
2262 	    # and removing) but not nvsets. In such a case, removing the nvset would
2263 	    # cause the whole change to be rejected by pacemaker with a "permission
2264 	    # denied" message.
2265 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
2266 	
2267 	    dom = dom_element.ownerDocument
2268 	    element_found = False
2269 	    for el in dom_element.getElementsByTagName("nvpair"):
2270 	        if el.getAttribute("name") == name:
2271 	            element_found = True
2272 	            if value == "":
2273 	                dom_element.removeChild(el)
2274 	            else:
2275 	                el.setAttribute("value", value)
2276 	            break
2277 	    if not element_found and value != "":
2278 	        el = dom.createElement("nvpair")
2279 	        el.setAttribute("id", id_prefix + name)
2280 	        el.setAttribute("name", name)
2281 	        el.setAttribute("value", value)
2282 	        dom_element.appendChild(el)
2283 	    return dom_element
2284 	
2285 	
2286 	# Passed an array of strings ["a=b","c=d"], return array of tuples
2287 	# [("a","b"),("c","d")]
2288 	def convert_args_to_tuples(ra_values):
2289 	    """
2290 	    Commandline options: no options
2291 	    """
2292 	    ret = []
2293 	    for ra_val in ra_values:
2294 	        if ra_val.count("=") != 0:
2295 	            split_val = ra_val.split("=", 1)
2296 	            ret.append((split_val[0], split_val[1]))
2297 	    return ret
2298 	
2299 	
2300 	def is_int(val):
2301 	    try:
2302 	        int(val)
2303 	        return True
2304 	    except ValueError:
2305 	        return False
2306 	
2307 	
2308 	def dom_update_utilization(dom_element, attributes, id_prefix=""):
2309 	    """
2310 	    Commandline options: no options
2311 	    """
2312 	    attr_tuples = []
2313 	    for name, value in sorted(attributes.items()):
2314 	        if value != "" and not is_int(value):
2315 	            err(
2316 	                "Value of utilization attribute must be integer: "
2317 	                "'{0}={1}'".format(name, value)
2318 	            )
2319 	        attr_tuples.append((name, value))
2320 	    dom_update_nvset(
2321 	        dom_element,
2322 	        attr_tuples,
2323 	        "utilization",
2324 	        id_prefix + dom_element.getAttribute("id") + "-utilization",
2325 	    )
2326 	
2327 	
2328 	def dom_update_meta_attr(dom_element, attributes):
2329 	    """
2330 	    Commandline options: no options
2331 	    """
2332 	    dom_update_nvset(
2333 	        dom_element,
2334 	        attributes,
2335 	        "meta_attributes",
2336 	        dom_element.getAttribute("id") + "-meta_attributes",
2337 	    )
2338 	
2339 	
2340 	def dom_update_instance_attr(dom_element, attributes):
2341 	    """
2342 	    Commandline options: no options
2343 	    """
2344 	    dom_update_nvset(
2345 	        dom_element,
2346 	        attributes,
2347 	        "instance_attributes",
2348 	        dom_element.getAttribute("id") + "-instance_attributes",
2349 	    )
2350 	
2351 	
2352 	def get_utilization(element, filter_name=None):
2353 	    """
2354 	    Commandline options: no options
2355 	    """
2356 	    utilization = {}
2357 	    for e in element.getElementsByTagName("utilization"):
2358 	        for u in e.getElementsByTagName("nvpair"):
2359 	            name = u.getAttribute("name")
2360 	            if filter_name is not None and name != filter_name:
2361 	                continue
2362 	            utilization[name] = u.getAttribute("value")
2363 	        # Use just first element of utilization attributes. We don't support
2364 	        # utilization with rules just yet.
2365 	        break
2366 	    return utilization
2367 	
2368 	
2369 	def get_utilization_str(element, filter_name=None):
2370 	    """
2371 	    Commandline options: no options
2372 	    """
2373 	    output = []
2374 	    for name, value in sorted(get_utilization(element, filter_name).items()):
2375 	        output.append(name + "=" + value)
2376 	    return " ".join(output)
2377 	
2378 	
2379 	def get_lib_env() -> LibraryEnvironment:
2380 	    """
2381 	    Commandline options:
2382 	      * -f - CIB file
2383 	      * --corosync_conf - corosync.conf file
2384 	      * --request-timeout - timeout of HTTP requests
2385 	    """
2386 	    user = None
2387 	    groups = None
2388 	    if os.geteuid() == 0:
2389 	        for name in ("CIB_user", "CIB_user_groups"):
2390 	            if name in os.environ and os.environ[name].strip():
2391 	                value = os.environ[name].strip()
2392 	                if name == "CIB_user":
2393 	                    user = value
2394 	                else:
2395 	                    groups = value.split(" ")
2396 	
2397 	    cib_data = None
2398 	    if usefile:
2399 	        cib_data = get_cib()
2400 	
2401 	    corosync_conf_data = None
2402 	    if "--corosync_conf" in pcs_options:
2403 	        conf = pcs_options["--corosync_conf"]
2404 	        try:
2405 	            with open(conf) as corosync_conf_file:
2406 	                corosync_conf_data = corosync_conf_file.read()
2407 	        except OSError as e:
2408 	            err("Unable to read %s: %s" % (conf, e.strerror))
2409 	
2410 	    return LibraryEnvironment(
2411 	        logging.getLogger("pcs"),
2412 	        get_report_processor(),
2413 	        user,
2414 	        groups,
2415 	        cib_data,
2416 	        corosync_conf_data,
2417 	        known_hosts_getter=read_known_hosts_file,
2418 	        request_timeout=pcs_options.get("--request-timeout"),
2419 	    )
2420 	
2421 	
2422 	def get_cib_user_groups():
2423 	    """
2424 	    Commandline options: no options
2425 	    """
2426 	    user = None
2427 	    groups = None
2428 	    if os.geteuid() == 0:
2429 	        for name in ("CIB_user", "CIB_user_groups"):
2430 	            if name in os.environ and os.environ[name].strip():
2431 	                value = os.environ[name].strip()
2432 	                if name == "CIB_user":
2433 	                    user = value
2434 	                else:
2435 	                    groups = value.split(" ")
2436 	    return user, groups
2437 	
2438 	
2439 	def get_cli_env() -> Env:
2440 	    """
2441 	    Commandline options:
2442 	      * --debug
2443 	      * --request-timeout
2444 	    """
2445 	    env = Env()
2446 	    env.user, env.groups = get_cib_user_groups()
2447 	    env.known_hosts_getter = read_known_hosts_file
2448 	    env.report_processor = get_report_processor()
2449 	    env.request_timeout = pcs_options.get("--request-timeout")
2450 	    return env
2451 	
2452 	
2453 	def get_middleware_factory():
2454 	    """
2455 	    Commandline options:
2456 	      * --corosync_conf
2457 	      * --name
2458 	      * --booth-conf
2459 	      * --booth-key
2460 	      * -f
2461 	    """
2462 	    return middleware.create_middleware_factory(
2463 	        cib=middleware.cib(filename if usefile else None, touch_cib_file),
2464 	        corosync_conf_existing=middleware.corosync_conf_existing(
2465 	            pcs_options.get("--corosync_conf")
2466 	        ),
2467 	        booth_conf=pcs.cli.booth.env.middleware_config(
2468 	            pcs_options.get("--booth-conf"),
2469 	            pcs_options.get("--booth-key"),
2470 	        ),
2471 	    )
2472 	
2473 	
2474 	def get_library_wrapper() -> Library:
2475 	    """
2476 	    Commandline options:
2477 	      * --debug
2478 	      * --request-timeout
2479 	      * --corosync_conf
2480 	      * --name
2481 	      * --booth-conf
2482 	      * --booth-key
2483 	      * -f
2484 	    NOTE: usage of options may depend on used middleware for particular command
2485 	    """
2486 	    return Library(get_cli_env(), get_middleware_factory())
2487 	
2488 	
2489 	def exit_on_cmdline_input_error(
2490 	    error: CmdLineInputError, main_name: str, usage_name: StringSequence
2491 	) -> None:
2492 	    if error and error.message:
2493 	        reports_output.error(error.message)
2494 	    if error and error.hint:
2495 	        print_to_stderr(f"Hint: {error.hint}")
2496 	    if not error or (not error.message or error.show_both_usage_and_message):
2497 	        usage.show(main_name, list(usage_name))
2498 	    sys.exit(1)
2499 	
2500 	
2501 	def get_report_processor() -> ReportProcessorToConsole:
2502 	    return ReportProcessorToConsole(debug="--debug" in pcs_options)
2503 	
2504 	
2505 	def get_user_and_pass() -> tuple[str, str]:
2506 	    """
2507 	    Commandline options:
2508 	      * -u - username
2509 	      * -p - password
2510 	    """
2511 	    username = (
2512 	        pcs_options["-u"]
2513 	        if "-u" in pcs_options
2514 	        else get_terminal_input("Username: ")
2515 	    )
2516 	    password = (
2517 	        pcs_options["-p"] if "-p" in pcs_options else get_terminal_password()
2518 	    )
2519 	    return username, password
2520 	
2521 	
2522 	def get_input_modifiers() -> InputModifiers:
2523 	    return InputModifiers(pcs_options)
2524 	
2525 	
2526 	def get_token_from_file(file_name: str) -> str:
2527 	    try:
2528 	        with open(file_name, "rb") as file:
2529 	            # 256 to stay backwards compatible
2530 	            max_size = 256
2531 	            value_bytes = file.read(max_size + 1)
2532 	            if len(value_bytes) > max_size:
2533 	                err(f"Maximal token size of {max_size} bytes exceeded")
2534 	            if not value_bytes:
2535 	                err(f"File '{file_name}' is empty")
2536 	            return base64.b64encode(value_bytes).decode("utf-8")
2537 	    except OSError as e:
2538 	        err(f"Unable to read file '{file_name}': {e}", exit_after_error=False)
2539 	        raise SystemExit(1) from e
2540 	
2541 	
2542 	def print_warning_if_utilization_attrs_has_no_effect(
2543 	    properties_facade: PropertyConfigurationFacade,
2544 	) -> None:
2545 	    PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS = [
2546 	        "balanced",
2547 	        "minimal",
2548 	        "utilization",
2549 	    ]
2550 	    value = properties_facade.get_property_value_or_default(
2551 	        "placement-strategy"
2552 	    )
2553 	    if value not in PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS:
2554 	        reports_output.warn(
2555 	            "Utilization attributes configuration has no effect until cluster "
2556 	            "property option 'placement-strategy' is set to one of the "
2557 	            "values: "
2558 	            f"{format_list(PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS)}"
2559 	        )
2560