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