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