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