diff --git a/cts/cli/regression.cibadmin.exp b/cts/cli/regression.cibadmin.exp
index 1cc3034366..f953eb8e72 100644
--- a/cts/cli/regression.cibadmin.exp
+++ b/cts/cli/regression.cibadmin.exp
@@ -1,89 +1,89 @@
=#=#=#= Begin test: Validate CIB =#=#=#=
=#=#=#= Current cib after: Validate CIB =#=#=#=
=#=#=#= End test: Validate CIB - OK (0) =#=#=#=
* Passed: cibadmin - Validate CIB
=#=#=#= Begin test: Digest calculation =#=#=#=
-Digest:
+01fdf92db1638e8a7e0d8a72ec114c9f
=#=#=#= End test: Digest calculation - OK (0) =#=#=#=
* Passed: cibadmin - Digest calculation
=#=#=#= Begin test: Require --force for CIB erasure =#=#=#=
cibadmin: The supplied command is considered dangerous. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed.
=#=#=#= Current cib after: Require --force for CIB erasure =#=#=#=
=#=#=#= End test: Require --force for CIB erasure - Operation not safe (107) =#=#=#=
* Passed: cibadmin - Require --force for CIB erasure
=#=#=#= Begin test: Allow CIB erasure with --force =#=#=#=
=#=#=#= End test: Allow CIB erasure with --force - OK (0) =#=#=#=
* Passed: cibadmin - Allow CIB erasure with --force
=#=#=#= Begin test: Query CIB =#=#=#=
=#=#=#= Current cib after: Query CIB =#=#=#=
=#=#=#= End test: Query CIB - OK (0) =#=#=#=
* Passed: cibadmin - Query CIB
diff --git a/cts/cts-cli.in b/cts/cts-cli.in
index d5338c3fe2..57602f98b1 100644
--- a/cts/cts-cli.in
+++ b/cts/cts-cli.in
@@ -1,3451 +1,3451 @@
#!@PYTHON@
"""Regression tests for Pacemaker's command line tools."""
# pylint doesn't like the module name "cts-cli" which is an invalid complaint for this file
# but probably something we want to continue warning about elsewhere
# pylint: disable=invalid-name
# pacemaker imports need to come after we modify sys.path, which pylint will complain about.
# pylint: disable=wrong-import-position
# We know this is a very long file.
# pylint: disable=too-many-lines
__copyright__ = "Copyright 2024-2025 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
import argparse
from contextlib import contextmanager
from datetime import datetime, timedelta
import fileinput
from functools import partial
from gettext import ngettext
from multiprocessing import Pool, cpu_count
import os
import pathlib
import re
from shutil import copyfile
import signal
from string import Formatter
import subprocess
import sys
from tempfile import NamedTemporaryFile, TemporaryDirectory, mkstemp
import types
# These imports allow running from a source checkout after running `make`.
if os.path.exists("@abs_top_srcdir@/python"):
sys.path.insert(0, "@abs_top_srcdir@/python")
# pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant
if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@":
sys.path.insert(0, "@abs_top_builddir@/python")
from pacemaker._cts.errors import XmlValidationError
from pacemaker._cts.validate import validate
from pacemaker.buildoptions import BuildOptions
from pacemaker.exitstatus import ExitStatus
# Individual tool tests are split out, but can also be accessed as a group with "tools"
tools_tests = ["cibadmin", "crm_attribute", "crm_standby", "crm_resource",
"crm_ticket", "crmadmin", "crm_shadow", "crm_verify", "crm_simulate",
"crm_diff"]
# The default list of tests to run, in the order they should be run
default_tests = ["access_render", "daemons", "dates", "error_codes"] + tools_tests + \
["crm_mon", "acls", "validity", "upgrade", "rules", "feature_set"]
other_tests = ["agents"]
# The directory containing this program
test_home = os.path.dirname(os.path.realpath(__file__))
# Where test data is stored
cts_cli_data = f"{test_home}/cli"
# The name of the shadow CIB
SHADOW_NAME = "cts-cli"
# Arguments to pass to valgrind
VALGRIND_ARGS = ["-q", "--gen-suppressions=all", "--show-reachable=no", "--leak-check=full",
"--trace-children=no", "--time-stamp=yes", "--num-callers=20",
f"--suppressions={test_home}/valgrind-pcmk.suppressions"]
class PluralFormatter(Formatter):
"""
Special string formatting class for selecting singular vs. plurals.
Use like so:
fmt = PluralFormatter()
print(fmt.format("{0} {0}:plural,test,tests} succeeded", n_tests))
"""
def format_field(self, value, format_spec):
"""Convert a value to a formatted representation."""
if format_spec.startswith("plural,"):
eles = format_spec.split(',')
if len(eles) == 2:
singular = eles[1]
plural = singular + "s"
else:
singular = eles[1]
plural = eles[2]
return ngettext(singular, plural, value)
return super().format_field(value, format_spec)
def cleanup_shadow_dir():
"""Remove any previously created shadow CIB directory."""
subprocess.run(["crm_shadow", "--force", "--delete", SHADOW_NAME],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
def copy_existing_cib(existing):
"""
Generate a CIB by copying an existing one to a temporary location.
This is suitable for use with the cib_gen= parameter to the TestGroup class.
"""
(fp, new) = mkstemp(prefix="cts-cli.cib.xml.")
os.close(fp)
copyfile(existing, new)
return new
def current_cib():
"""Return the complete current CIB."""
with environ({"CIB_user": "root"}):
return subprocess.check_output(["cibadmin", "-Q"], encoding="utf-8")
def make_test_group(desc, cmd, **kwargs):
"""
Create a TestGroup that replicates the same test for multiple classes.
The given description, cmd, and kwargs will be passed as arguments to each
Test subclass. The resulting objects will then be added to a TestGroup
and returned.
The main purpose of this function is to be able to run the same test for
both text and XML formats without having to duplicate everything.
"""
tests = []
for c in [Test, ValidatingTest]:
# Insert "--output-as=" after the command name.
splitup = cmd.split()
splitup.insert(1, c.format_args)
obj = c(desc, " ".join(splitup), **kwargs)
tests.append(obj)
return TestGroup(tests)
def create_shadow_cib(shadow_dir, create_empty=True, validate_with=None,
valgrind=False):
"""
Create a shadow CIB file.
Keyword arguments:
create_empty -- If True, the shadow CIB will be empty. Otherwise, the
shadow CIB will be a copy of the currently active
cluster configuration.
validate_with -- If not None, the schema version to validate the CIB
against
valgrind -- If True, run the create operation under valgrind
"""
args = ["crm_shadow", "--batch", "--force"]
if create_empty:
args += ["--create-empty", SHADOW_NAME]
else:
args += ["--create", SHADOW_NAME]
if validate_with is not None:
args += ["--validate-with", validate_with]
if valgrind:
args = ["valgrind"] + VALGRIND_ARGS + args
os.environ["CIB_shadow_dir"] = shadow_dir
os.environ["CIB_shadow"] = SHADOW_NAME
subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
delete_shadow_resource_defaults()
def delete_shadow_resource_defaults():
"""Clear out the rsc_defaults section from a shadow CIB file."""
# A newly created empty CIB might or might not have a rsc_defaults section
# depending on whether the --with-resource-stickiness-default configure
# option was used. To ensure regression tests behave the same either way,
# delete any rsc_defaults after creating or erasing a CIB.
subprocess.run(["cibadmin", "--delete", "--xml-text", ""],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
# The above command might or might not bump the CIB version, so reset it
# to ensure future changes result in the same version for comparison.
reset_shadow_cib_version()
def reset_shadow_cib_version():
"""Set various version numbers in a shadow CIB file back to 0."""
with fileinput.input(files=[shadow_path()], inplace=True) as f:
for line in f:
line = re.sub('epoch="[0-9]*"', 'epoch="1"', line)
line = re.sub('num_updates="[0-9]*"', 'num_updates="0"', line)
line = re.sub('admin_epoch="[0-9]*"', 'admin_epoch="0"', line)
print(line, end='')
def run_cmd_list(cmds):
"""
Run one or more shell commands.
cmds can be:
* A string
* A Python function
* A list of the above
Raises subprocess.CalledProcessError on error.
"""
if cmds is None:
return
if isinstance(cmds, (str, types.FunctionType)):
cmds = [cmds]
for c in cmds:
if isinstance(c, types.FunctionType):
c()
else:
subprocess.run(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, universal_newlines=True, check=True)
def sanitize_output(s):
"""
Replace content in the output expected to change between test runs.
This is stuff like version numbers, timestamps, source line numbers,
build options, system names and messages, etc.
"""
# A list of tuples of regular expressions and their replacements.
replacements = [
(r'Created new pacemaker-.* configuration', r'Created new pacemaker configuration'),
(r'Device not configured', r'No such device or address'),
(r'^Entity: line [0-9]+: ', r''),
(r'(Injecting attribute last-failure-ping#monitor_10000=)[0-9]*', r'\1'),
(r'Last change: .*', r'Last change:'),
(r'Last updated: .*', r'Last updated:'),
(r'^Migration will take effect until: .*', r'Migration will take effect until:'),
(r'(\* Possible values.*: .*)\(default: [^)]*\)', r'\1(default: )'),
(r"""-X '.*'""", r"""-X '...'"""),
(r' api-version="[^"]*"', r' api-version="X"'),
(r'\(apply_upgrade@.*\.c:[0-9]+\)', r'apply_upgrade'),
(r'\(invert_action@.*\.c:[0-9]+\)', r'invert_action'),
(r'\(pcmk__update_schema@.*\.c:[0-9]+\)', r'pcmk__update_schema'),
(r'(
"""
# Create a test CIB that has ACL roles
basic_tests = [
Test("Configure some ACLs", "cibadmin -M -o acls -p", update_cib=True,
stdin=acl_cib),
Test("Enable ACLs", "crm_attribute -n enable-acl -v true",
update_cib=True),
# Run cibadmin --show-access on the test CIB as an ACL-restricted user
Test("An instance of ACLs render (into color)",
"cibadmin --force --show-access=color -Q --user tony"),
Test("An instance of ACLs render (into namespacing)",
"cibadmin --force --show-access=namespace -Q --user tony"),
Test("An instance of ACLs render (into text)",
"cibadmin --force --show-access=text -Q --user tony"),
]
return [
ShadowTestGroup(basic_tests),
]
class DaemonsRegressionTest(RegressionTest):
"""A class for testing command line options of pacemaker daemons."""
@property
def name(self):
"""Return the name of this regression test."""
return "daemons"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
return [
Test("Get CIB manager metadata", "pacemaker-based metadata"),
Test("Get controller metadata", "pacemaker-controld metadata"),
Test("Get fencer metadata", "pacemaker-fenced metadata"),
Test("Get scheduler metadata", "pacemaker-schedulerd metadata"),
]
class DatesRegressionTest(RegressionTest):
"""A class for testing handling of ISO8601 dates."""
@property
def name(self):
"""Return the name of this regression test."""
return "dates"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
invalid_periods = [
"",
"2019-01-01 00:00:00Z", # Start with no end
"2019-01-01 00:00:00Z/", # Start with only a trailing slash
"PT2S/P1M", # Two durations
"2019-13-01 00:00:00Z/P1M", # Out-of-range month
"20191077T15/P1M", # Out-of-range day
"2019-10-01T25:00:00Z/P1M", # Out-of-range hour
"2019-10-01T24:00:01Z/P1M", # Hour 24 with anything but :00:00
"PT5H/20191001T007000Z", # Out-of-range minute
"2019-10-01 00:00:80Z/P1M", # Out-of-range second
"2019-10-01 00:00:10 +25:00/P1M", # Out-of-range offset hour
"20191001T000010 -00:61/P1M", # Out-of-range offset minute
"P1Y/2019-02-29 00:00:00Z", # Feb. 29 in non-leap-year
"2019-01-01 00:00:00Z/P", # Duration with no values
"P1Z/2019-02-20 00:00:00Z", # Invalid duration unit
"P1YM/2019-02-20 00:00:00Z", # No number for duration unit
]
# Ensure invalid period specifications are rejected
invalid_period_tests = []
for p in invalid_periods:
invalid_period_tests.append(Test(f"Invalid period - [{p}]",
f"iso8601 -p '{p}'",
expected_rc=ExitStatus.INVALID_PARAM))
year_tests = []
for y in ["06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "40"]:
year_tests.extend([
Test(f"20{y}-W01-7",
f"iso8601 -d '20{y}-W01-7 00Z'"),
Test(f"20{y}-W01-7 - round-trip",
f"iso8601 -d '20{y}-W01-7 00Z' -W -E '20{y}-W01-7 00:00:00Z'"),
Test(f"20{y}-W01-1",
f"iso8601 -d '20{y}-W01-1 00Z'"),
Test(f"20{y}-W01-1 - round-trip",
f"iso8601 -d '20{y}-W01-1 00Z' -W -E '20{y}-W01-1 00:00:00Z'")
])
return invalid_period_tests + [
make_test_group("'2005-040/2005-043' period", "iso8601 -p '2005-040/2005-043'"),
Test("2014-01-01 00:30:00 - 1 Hour",
"iso8601 -d '2014-01-01 00:30:00Z' -D P-1H -E '2013-12-31 23:30:00Z'"),
Test("Valid date - Feb 29 in leap year",
"iso8601 -d '2020-02-29 00:00:00Z' -E '2020-02-29 00:00:00Z'"),
Test("Valid date - using 'T' and offset",
"iso8601 -d '20191201T131211 -05:00' -E '2019-12-01 18:12:11Z'"),
Test("24:00:00 equivalent to 00:00:00 of next day",
"iso8601 -d '2019-12-31 24:00:00Z' -E '2020-01-01 00:00:00Z'"),
] + year_tests + [
make_test_group("2009-W53-07",
"iso8601 -d '2009-W53-7 00:00:00Z' -W -E '2009-W53-7 00:00:00Z'"),
Test("epoch + 2 Years 5 Months 6 Minutes",
"iso8601 -d 'epoch' -D P2Y5MT6M -E '1972-06-01 00:06:00Z'"),
Test("2009-01-31 + 1 Month",
"iso8601 -d '20090131T000000Z' -D P1M -E '2009-02-28 00:00:00Z'"),
Test("2009-01-31 + 2 Months",
"iso8601 -d '2009-01-31 00:00:00Z' -D P2M -E '2009-03-31 00:00:00Z'"),
Test("2009-01-31 + 3 Months",
"iso8601 -d '2009-01-31 00:00:00Z' -D P3M -E '2009-04-30 00:00:00Z'"),
make_test_group("2009-03-31 - 1 Month",
"iso8601 -d '2009-03-31 01:00:00 +01:00' -D P-1M -E '2009-02-28 00:00:00Z'"),
make_test_group("2038-01-01 + 3 Months",
"iso8601 -d '2038-01-01 00:00:00Z' -D P3M -E '2038-04-01 00:00:00Z'"),
]
class ErrorCodeRegressionTest(RegressionTest):
"""A class for testing error code reporting."""
@property
def name(self):
"""Return the name of this regression test."""
return "error_codes"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
# Legacy return codes
#
# Don't test unknown legacy code. FreeBSD includes a colon in strerror(),
# while other distros do not.
legacy_tests = [
make_test_group("Get legacy return code", "crm_error 201"),
make_test_group("Get legacy return code (with name)", "crm_error -n 201"),
make_test_group("Get multiple legacy return codes", "crm_error 201 202"),
make_test_group("Get multiple legacy return codes (with names)",
"crm_error -n 201 202"),
# We can only rely on our custom codes, so we'll spot-check codes 201-209
Test("List legacy return codes (spot check)",
"crm_error -l | grep 20[1-9]"),
ValidatingTest("List legacy return codes (spot check)",
"crm_error -l --output-as=xml | grep -Ev '&1 | sed -e 's/Digest:.*/Digest:/'"),
+ Test("Digest calculation", "cibadmin -Q | cibadmin -5 -p 2>&1"),
Test("Require --force for CIB erasure", "cibadmin -E",
expected_rc=ExitStatus.UNSAFE, update_cib=True),
Test("Allow CIB erasure with --force", "cibadmin -E --force"),
# Verify the output after erasure
Test("Query CIB", "cibadmin -Q",
setup=delete_shadow_resource_defaults,
update_cib=True),
]
# Add some stuff to the empty CIB so we know that erasing it did something.
basic_tests_setup = [
"""cibadmin -C -o nodes --xml-text ''""",
"""cibadmin -C -o crm_config --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''"""
]
return [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
]
class CrmAttributeRegressionTest(RegressionTest):
"""A class for testing crm_attribute."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_attribute"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
options_tests = [
make_test_group("List all available options (invalid type)",
"crm_attribute --list-options=asdf",
expected_rc=ExitStatus.USAGE),
make_test_group("List non-advanced cluster options",
"crm_attribute --list-options=cluster"),
make_test_group("List all available cluster options",
"crm_attribute --list-options=cluster --all"),
Test("Return usage error if both -p and OCF_RESOURCE_INSTANCE are empty strings",
"crm_attribute -N cluster01 -p '' -G",
expected_rc=ExitStatus.USAGE),
]
value_update_tests = [
Test("Query the value of an attribute that does not exist",
"crm_attribute -n ABCD --query --quiet",
expected_rc=ExitStatus.NOSUCH),
Test("Configure something before erasing",
"crm_attribute -n test_attr -v 5", update_cib=True),
Test("Test '++' XML attribute update syntax",
"""cibadmin -M --score --xml-text=''""",
update_cib=True),
Test("Test '+=' XML attribute update syntax",
"""cibadmin -M --score --xml-text=''""",
update_cib=True),
make_test_group("Test '++' nvpair value update syntax",
"crm_attribute -n test_attr -v 'value++' --score",
update_cib=True),
make_test_group("Test '+=' nvpair value update syntax",
"crm_attribute -n test_attr -v 'value+=2' --score",
update_cib=True),
Test("Test '++' XML attribute update syntax (--score not set)",
"""cibadmin -M --xml-text=''""",
update_cib=True),
Test("Test '+=' XML attribute update syntax (--score not set)",
"""cibadmin -M --xml-text=''""",
update_cib=True),
make_test_group("Test '++' nvpair value update syntax (--score not set)",
"crm_attribute -n test_attr -v 'value++'",
update_cib=True),
make_test_group("Test '+=' nvpair value update syntax (--score not set)",
"crm_attribute -n test_attr -v 'value+=2'",
update_cib=True),
]
query_set_tests = [
Test("Set cluster option", "crm_attribute -n cluster-delay -v 60s",
update_cib=True),
Test("Query new cluster option",
"cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay"),
Test("Set no-quorum policy",
"crm_attribute -n no-quorum-policy -v ignore", update_cib=True),
Test("Delete nvpair",
"""cibadmin -D -o crm_config --xml-text ''""",
update_cib=True),
Test("Create operation should fail",
"""cibadmin -C -o crm_config --xml-text ''""",
expected_rc=ExitStatus.EXISTS, update_cib=True),
Test("Modify cluster options section",
"""cibadmin -M -o crm_config --xml-text ''""",
update_cib=True),
Test("Query updated cluster option",
"cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay",
update_cib=True),
Test("Set duplicate cluster option",
"crm_attribute -n cluster-delay -v 40s -s duplicate",
update_cib=True),
Test("Setting multiply defined cluster option should fail",
"crm_attribute -n cluster-delay -v 30s",
expected_rc=ExitStatus.MULTIPLE, update_cib=True),
Test("Set cluster option with -s",
"crm_attribute -n cluster-delay -v 30s -s duplicate",
update_cib=True),
Test("Delete cluster option with -i",
"crm_attribute -n cluster-delay -D -i cib-bootstrap-options-cluster-delay",
update_cib=True),
Test("Create node1 and bring it online",
"crm_simulate --live-check --in-place --node-up=node1",
update_cib=True),
Test("Create node attribute",
"crm_attribute -n ram -v 1024M -N node1 -t nodes",
update_cib=True),
Test("Query new node attribute",
"cibadmin -Q -o nodes | grep node1-ram",
update_cib=True),
Test("Create second node attribute",
"crm_attribute -n rattr -v XYZ -N node1 -t nodes",
update_cib=True),
Test("Query node attributes by pattern",
"crm_attribute -t nodes -P 'ra.*' -N node1 --query"),
Test("Update node attributes by pattern",
"crm_attribute -t nodes -P 'rat.*' -N node1 -v 10",
update_cib=True),
Test("Delete node attributes by pattern",
"crm_attribute -t nodes -P 'rat.*' -N node1 -D",
update_cib=True),
Test("Set a transient (fail-count) node attribute",
"crm_attribute -n fail-count-foo -v 3 -N node1 -t status",
update_cib=True),
Test("Query a fail count", "crm_failcount --query -r foo -N node1",
update_cib=True),
Test("Show node attributes with crm_simulate",
"crm_simulate --live-check --show-attrs"),
Test("Set a second transient node attribute",
"crm_attribute -n fail-count-bar -v 5 -N node1 -t status",
update_cib=True),
Test("Query transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 --query"),
Test("Update transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 -v 10",
update_cib=True),
Test("Delete transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 -D",
update_cib=True),
Test("crm_attribute given invalid delete usage",
"crm_attribute -t nodes -N node1 -D",
expected_rc=ExitStatus.USAGE),
Test("Set a utilization node attribute",
"crm_attribute -n cpu -v 1 -N node1 -z",
update_cib=True),
Test("Query utilization node attribute",
"crm_attribute --query -n cpu -N node1 -z"),
# This update will fail because it has version numbers
Test("Replace operation should fail",
"""cibadmin -Q | sed -e 's/epoch="[^"]*"/epoch="1"/' | cibadmin -R -p""",
expected_rc=ExitStatus.OLD),
]
promotable_tests = [
make_test_group("Query a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Delete a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -D"),
make_test_group("Query after deleting a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Update a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -v 1"),
make_test_group("Query after updating a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G"),
make_test_group("Update an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -v 5"),
make_test_group("Query after updating an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G"),
make_test_group("Delete an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -D"),
make_test_group("Query after deleting an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
]
# Test for an issue with legacy command line parsing when the resource is
# specified in the environment (CLBZ#5509)
ocf_rsc_instance_tests = [
make_test_group("Update a promotable score attribute to -INFINITY",
"crm_attribute -N cluster01 -p -v -INFINITY",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
make_test_group("Query after updating a promotable score attribute to -INFINITY",
"crm_attribute -N cluster01 -p -G",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
Test("Try OCF_RESOURCE_INSTANCE if -p is specified with an empty string",
"crm_attribute -N cluster01 -p '' -G",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
]
return options_tests + [
ShadowTestGroup(value_update_tests),
ShadowTestGroup(query_set_tests),
TestGroup(promotable_tests + ocf_rsc_instance_tests,
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"},
cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
]
class CrmStandbyRegressionTest(RegressionTest):
"""A class for testing crm_standby."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_standby"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
Test("Default standby value", "crm_standby -N node1 -G"),
Test("Set standby status", "crm_standby -N node1 -v true",
update_cib=True),
Test("Query standby value", "crm_standby -N node1 -G"),
Test("Delete standby value", "crm_standby -N node1 -D",
update_cib=True),
]
return [
ShadowTestGroup(basic_tests,
setup="""cibadmin -C -o nodes --xml-text ''"""),
]
class CrmResourceRegressionTest(RegressionTest):
"""A class for testing crm_resource."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_resource"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
options_tests = [
Test("crm_resource run with extra arguments", "crm_resource foo bar",
expected_rc=ExitStatus.USAGE),
Test("List all available resource options (invalid type)",
"crm_resource --list-options=asdf",
expected_rc=ExitStatus.USAGE),
Test("List all available resource options (invalid type)",
"crm_resource --list-options=asdf --output-as=xml",
expected_rc=ExitStatus.USAGE),
make_test_group("List non-advanced primitive meta-attributes",
"crm_resource --list-options=primitive"),
make_test_group("List all available primitive meta-attributes",
"crm_resource --list-options=primitive --all"),
make_test_group("List non-advanced fencing parameters",
"crm_resource --list-options=fencing"),
make_test_group("List all available fencing parameters",
"crm_resource --list-options=fencing --all"),
]
basic_tests = [
Test("Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
Test("crm_resource given both -r and resource config",
"crm_resource -r xyz --class ocf --provider pacemaker --agent Dummy",
expected_rc=ExitStatus.USAGE),
Test("crm_resource given resource config with invalid action",
"crm_resource --class ocf --provider pacemaker --agent Dummy -D",
expected_rc=ExitStatus.USAGE),
Test("Create a resource meta attribute",
"crm_resource -r dummy --meta -p is-managed -v false",
update_cib=True),
Test("Query a resource meta attribute",
"crm_resource -r dummy --meta -g is-managed",
update_cib=True),
Test("Remove a resource meta attribute",
"crm_resource -r dummy --meta -d is-managed",
update_cib=True),
ValidatingTest("Create another resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped --output-as=xml"),
ValidatingTest("Show why a resource is not running",
"crm_resource -Y -r dummy --output-as=xml"),
ValidatingTest("Remove another resource meta attribute",
"crm_resource -r dummy --meta -d target-role --output-as=xml"),
ValidatingTest("Get a non-existent attribute from a resource element",
"crm_resource -r dummy --get-parameter nonexistent --element --output-as=xml"),
make_test_group("Get a non-existent attribute from a resource element",
"crm_resource -r dummy --get-parameter nonexistent --element",
update_cib=True),
Test("Get an existent attribute from a resource element",
"crm_resource -r dummy --get-parameter class --element",
update_cib=True),
ValidatingTest("Set a non-existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml",
update_cib=True),
ValidatingTest("Set an existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml",
update_cib=True),
ValidatingTest("Delete an existent attribute for a resource element",
"crm_resource -r dummy -d description --element --output-as=xml",
update_cib=True),
ValidatingTest("Delete a non-existent attribute for a resource element",
"crm_resource -r dummy -d description --element --output-as=xml",
update_cib=True),
Test("Set a non-existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element",
update_cib=True),
Test("Set an existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element",
update_cib=True),
Test("Delete an existent attribute for a resource element",
"crm_resource -r dummy -d description --element",
update_cib=True),
Test("Delete a non-existent attribute for a resource element",
"crm_resource -r dummy -d description --element",
update_cib=True),
Test("Create a resource attribute", "crm_resource -r dummy -p delay -v 10s",
update_cib=True),
make_test_group("List the configured resources", "crm_resource -L",
update_cib=True),
Test("Implicitly list the configured resources", "crm_resource"),
Test("List IDs of instantiated resources", "crm_resource -l"),
make_test_group("Show XML configuration of resource", "crm_resource -q -r dummy"),
Test("Require a destination when migrating a resource that is stopped",
"crm_resource -r dummy -M",
update_cib=True, expected_rc=ExitStatus.USAGE),
Test("Don't support migration to non-existent locations",
"crm_resource -r dummy -M -N i.do.not.exist",
update_cib=True, expected_rc=ExitStatus.NOSUCH),
Test("Create a fencing resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
Test("Bring resources online", "crm_simulate --live-check --in-place",
update_cib=True),
Test("Try to move a resource to its existing location",
"crm_resource -r dummy --move --node node1",
update_cib=True, expected_rc=ExitStatus.EXISTS),
Test("Try to move a resource that doesn't exist",
"crm_resource -r xyz --move --node node1",
expected_rc=ExitStatus.NOSUCH),
Test("Move a resource from its existing location",
"crm_resource -r dummy --move",
update_cib=True),
Test("Clear out constraints generated by --move",
"crm_resource -r dummy --clear",
update_cib=True),
Test("Ban a resource on unknown node",
"crm_resource -r dummy -B -N host1",
expected_rc=ExitStatus.NOSUCH),
Test("Create two more nodes and bring them online",
"crm_simulate --live-check --in-place --node-up=node2 --node-up=node3",
update_cib=True),
Test("Ban dummy from node1", "crm_resource -r dummy -B -N node1",
update_cib=True),
Test("Show where a resource is running", "crm_resource -r dummy -W"),
Test("Show constraints on a resource", "crm_resource -a -r dummy"),
ValidatingTest("Ban dummy from node2",
"crm_resource -r dummy -B -N node2 --output-as=xml",
update_cib=True),
Test("Relocate resources due to ban",
"crm_simulate --live-check --in-place -S",
update_cib=True),
ValidatingTest("Move dummy to node1",
"crm_resource -r dummy -M -N node1 --output-as=xml",
update_cib=True),
Test("Clear implicit constraints for dummy on node2",
"crm_resource -r dummy -U -N node2",
update_cib=True),
Test("Drop the status section",
"cibadmin -R -o status --xml-text ''"),
Test("Create a clone",
"""cibadmin -C -o resources --xml-text ''"""),
Test("Create a resource meta attribute",
"crm_resource -r test-primitive --meta -p is-managed -v false",
update_cib=True),
Test("Create a resource meta attribute in the primitive",
"crm_resource -r test-primitive --meta -p is-managed -v false --force",
update_cib=True),
Test("Update resource meta attribute with duplicates",
"crm_resource -r test-clone --meta -p is-managed -v true",
update_cib=True),
Test("Update resource meta attribute with duplicates (force clone)",
"crm_resource -r test-clone --meta -p is-managed -v true --force",
update_cib=True),
Test("Update child resource meta attribute with duplicates",
"crm_resource -r test-primitive --meta -p is-managed -v false",
update_cib=True),
Test("Delete resource meta attribute with duplicates",
"crm_resource -r test-clone --meta -d is-managed",
update_cib=True),
Test("Delete resource meta attribute in parent",
"crm_resource -r test-primitive --meta -d is-managed",
update_cib=True),
Test("Create a resource meta attribute in the primitive",
"crm_resource -r test-primitive --meta -p is-managed -v false --force",
update_cib=True),
Test("Update existing resource meta attribute",
"crm_resource -r test-clone --meta -p is-managed -v true",
update_cib=True),
Test("Create a resource meta attribute in the parent",
"crm_resource -r test-clone --meta -p is-managed -v true --force",
update_cib=True),
Test("Delete resource parent meta attribute (force)",
"crm_resource -r test-clone --meta -d is-managed --force",
update_cib=True),
# Restore meta-attributes before running this test
Test("Delete resource child meta attribute",
"crm_resource -r test-primitive --meta -d is-managed",
setup=["crm_resource -r test-primitive --meta -p is-managed -v true --force",
"crm_resource -r test-clone --meta -p is-managed -v true --force"],
update_cib=True),
Test("Create the dummy-group resource group",
"""cibadmin -C -o resources --xml-text '"""
""""""
""""""
"""'""",
update_cib=True),
Test("Create a resource meta attribute in dummy1",
"crm_resource -r dummy1 --meta -p is-managed -v true",
update_cib=True),
Test("Create a resource meta attribute in dummy-group",
"crm_resource -r dummy-group --meta -p is-managed -v false",
update_cib=True),
Test("Delete the dummy-group resource group",
"cibadmin -D -o resources --xml-text ''",
update_cib=True),
Test("Specify a lifetime when moving a resource",
"crm_resource -r dummy --move --node node2 --lifetime=PT1H",
update_cib=True),
Test("Try to move a resource previously moved with a lifetime",
"crm_resource -r dummy --move --node node1",
update_cib=True),
Test("Ban dummy from node1 for a short time",
"crm_resource -r dummy -B -N node1 --lifetime=PT1S",
update_cib=True),
Test("Remove expired constraints",
"sleep 2 && crm_resource --clear --expired",
update_cib=True),
# Clear has already been tested elsewhere, but we need to get rid of the
# constraints so testing delete works. It won't delete if there's still
# a reference to the resource somewhere.
Test("Clear all implicit constraints for dummy",
"crm_resource -r dummy -U",
update_cib=True),
Test("Set a node health strategy",
"crm_attribute -n node-health-strategy -v migrate-on-red",
update_cib=True),
Test("Set a node health attribute",
"crm_attribute -N node3 -n '#health-cts-cli' -v red",
update_cib=True),
ValidatingTest("Show why a resource is not running on an unhealthy node",
"crm_resource -N node3 -Y -r dummy --output-as=xml"),
Test("Delete a resource",
"crm_resource -D -r dummy -t primitive",
update_cib=True),
]
constraint_tests = []
for rsc in ["prim1", "prim2", "prim3", "prim4", "prim5", "prim6", "prim7",
"prim8", "prim9", "prim10", "prim11", "prim12", "prim13",
"group", "clone"]:
constraint_tests.extend([
make_test_group(f"Check locations and constraints for {rsc}",
f"crm_resource -a -r {rsc}"),
make_test_group(f"Recursively check locations and constraints for {rsc}",
f"crm_resource -A -r {rsc}"),
])
constraint_tests.extend([
Test("Check locations and constraints for group member (referring to group)",
"crm_resource -a -r gr2"),
Test("Check locations and constraints for group member (without referring to group)",
"crm_resource -a -r gr2 --force"),
])
colocation_tests = [
ValidatingTest("Set a meta-attribute for primitive and resources colocated with it",
"crm_resource -r prim5 --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"),
Test("Set a meta-attribute for group and resource colocated with it",
"crm_resource -r group --meta --set-parameter=target-role -v Stopped --recursive"),
ValidatingTest("Set a meta-attribute for clone and resource colocated with it",
"crm_resource -r clone --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"),
]
digest_tests = [
ValidatingTest("Show resource digests",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml"),
Test("Show resource digests with overrides",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml CRM_meta_interval=10000 CRM_meta_timeout=20000"),
make_test_group("Show resource operations", "crm_resource --list-operations"),
]
basic2_tests = [
make_test_group("List a promotable clone resource",
"crm_resource --locate -r promotable-clone"),
make_test_group("List the primitive of a promotable clone resource",
"crm_resource --locate -r promotable-rsc"),
make_test_group("List a single instance of a promotable clone resource",
"crm_resource --locate -r promotable-rsc:0"),
make_test_group("List another instance of a promotable clone resource",
"crm_resource --locate -r promotable-rsc:1"),
Test("Try to move an instance of a cloned resource",
"crm_resource -r promotable-rsc:0 --move --node cluster01",
expected_rc=ExitStatus.INVALID_PARAM),
]
basic_tests_setup = [
"crm_attribute -n no-quorum-policy -v ignore",
"crm_simulate --live-check --in-place --node-up=node1"
]
return options_tests + [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
TestGroup(constraint_tests, env={"CIB_file": f"{cts_cli_data}/constraints.xml"}),
TestGroup(colocation_tests, cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/constraints.xml")),
TestGroup(digest_tests, env={"CIB_file": f"{cts_cli_data}/crm_resource_digests.xml"}),
TestGroup(basic2_tests, env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ValidatingTest("Check that CIB_file=\"-\" works - crm_resource",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crm_resource_digests.xml")),
]
class CrmTicketRegressionTest(RegressionTest):
"""A class for testing crm_ticket."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_ticket"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
Test("Default ticket granted state",
"crm_ticket -t ticketA -G granted -d false"),
Test("Set ticket granted state", "crm_ticket -t ticketA -r --force",
update_cib=True),
make_test_group("List ticket IDs", "crm_ticket -w"),
make_test_group("Query ticket state", "crm_ticket -t ticketA -q"),
make_test_group("Query ticket granted state",
"crm_ticket -t ticketA -G granted"),
Test("Delete ticket granted state",
"crm_ticket -t ticketA -D granted --force",
update_cib=True),
Test("Make a ticket standby", "crm_ticket -t ticketA -s",
update_cib=True),
Test("Query ticket standby state", "crm_ticket -t ticketA -G standby"),
Test("Activate a ticket", "crm_ticket -t ticketA -a",
update_cib=True),
make_test_group("List ticket details", "crm_ticket -L -t ticketA"),
Test("Add a second ticket", "crm_ticket -t ticketB -G granted -d false",
update_cib=True),
Test("Set second ticket granted state",
"crm_ticket -t ticketB -r --force",
update_cib=True),
make_test_group("List tickets", "crm_ticket -l"),
Test("Delete second ticket",
"""cibadmin --delete --xml-text ''""",
update_cib=True),
Test("Delete ticket standby state", "crm_ticket -t ticketA -D standby",
update_cib=True),
Test("Add a constraint to a ticket",
"""cibadmin -C -o constraints --xml-text ''""",
update_cib=True),
make_test_group("Query ticket constraints", "crm_ticket -t ticketA -c"),
Test("Delete ticket constraint",
"""cibadmin --delete --xml-text ''""",
update_cib=True),
]
basic_tests_setup = [
"""cibadmin -C -o crm_config --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''"""
]
return [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
]
class CrmadminRegressionTest(RegressionTest):
"""A class for testing crmadmin."""
@property
def name(self):
"""Return the name of this regression test."""
return "crmadmin"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
make_test_group("List all nodes", "crmadmin -N"),
make_test_group("Minimally list all nodes", "crmadmin -N -q"),
Test("List all nodes as bash exports", "crmadmin -N -B"),
make_test_group("List cluster nodes",
"crmadmin -N cluster"),
make_test_group("List guest nodes",
"crmadmin -N guest"),
make_test_group("List remote nodes",
"crmadmin -N remote"),
make_test_group("List cluster,remote nodes",
"crmadmin -N cluster,remote"),
make_test_group("List guest,remote nodes",
"crmadmin -N guest,remote"),
]
return [
TestGroup(basic_tests,
env={"CIB_file": f"{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml"}),
Test("Check that CIB_file=\"-\" works", "crmadmin -N",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml")),
]
class CrmShadowRegressionTest(RegressionTest):
"""A class for testing crm_shadow."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_shadow"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
no_instance_tests = [
make_test_group("Get active shadow instance (no active instance)",
"crm_shadow --which",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's file name (no active instance)",
"crm_shadow --file",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's contents (no active instance)",
"crm_shadow --display",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (no active instance)",
"crm_shadow --diff",
expected_rc=ExitStatus.NOSUCH),
]
# Create new shadow instance based on active CIB
# Don't use create_shadow_cib() here; test explicitly
new_instance_tests = [
make_test_group("Create copied shadow instance",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
# Query shadow instance based on active CIB
make_test_group("Get active shadow instance (copied)",
"crm_shadow --which"),
make_test_group("Get active shadow instance's file name (copied)",
"crm_shadow --file"),
make_test_group("Get active shadow instance's contents (copied)",
"crm_shadow --display"),
make_test_group("Get active shadow instance's diff (copied)",
"crm_shadow --diff"),
]
# Make some changes to the shadow file
modify_cib = """export CIB_file=$(crm_shadow --file) && """ \
"""cibadmin --modify --xml-text '' && """ \
"""cibadmin --delete --xml-text '' && """ \
"""cibadmin --create -o resources --xml-text '' && """ \
"""cibadmin --create -o status --xml-text ''"""
more_tests = [
# We can't use make_test_group() here because we only want to run
# the modify_cib setup code once, and make_test_group will pass all
# kwargs to every instance it creates.
Test("Get active shadow instance's diff (after changes)",
"crm_shadow --diff",
setup=modify_cib, expected_rc=ExitStatus.ERROR),
ValidatingTest("Get active shadow instance's diff (after changes)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
TestGroup([
# Commit the modified shadow CIB to a temp active CIB file
Test("Commit shadow instance",
f"crm_shadow --commit {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Commit shadow instance (force)",
f"crm_shadow --commit {SHADOW_NAME} --force"),
Test("Get active shadow instance's diff (after commit)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
Test("Commit shadow instance (force) (all)",
f"crm_shadow --commit {SHADOW_NAME} --force --all"),
Test("Get active shadow instance's diff (after commit all)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
], cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
TestGroup([
# Repeat sequence with XML output
ValidatingTest("Commit shadow instance",
f"crm_shadow --commit {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Commit shadow instance (force)",
f"crm_shadow --commit {SHADOW_NAME} --force --output-as=xml"),
ValidatingTest("Get active shadow instance's diff (after commit)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
ValidatingTest("Commit shadow instance (force) (all)",
f"crm_shadow --commit {SHADOW_NAME} --force --all --output-as=xml"),
ValidatingTest("Get active shadow instance's diff (after commit all)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
# Commit an inactive shadow instance with no active instance
make_test_group("Commit shadow instance (no active instance)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_shadow": None},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (no active instance) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Commit an inactive shadow instance with an active instance
make_test_group("Commit shadow instance (mismatch)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (mismatch) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Commit an active shadow instance whose shadow file is missing
make_test_group("Commit shadow instance (nonexistent shadow file)",
"crm_shadow --commit nonexistent_shadow",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (nonexistent shadow file) (force)",
"crm_shadow --commit nonexistent_shadow --force",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (nonexistent shadow file)",
"crm_shadow --diff",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.NOSUCH),
# Commit an active shadow instance when the CIB file is missing
make_test_group("Commit shadow instance (nonexistent CIB file)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (nonexistent CIB file)",
"crm_shadow --diff",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.NOSUCH),
], cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
]
delete_1_tests = [
# Delete an active shadow instance
Test("Delete shadow instance", f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (force)", f"crm_shadow --delete {SHADOW_NAME} --force"),
ShadowTestGroup([
ValidatingTest("Delete shadow instance",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
])
]
delete_2_tests = [
# Delete an inactive shadow instance with no active instance
Test("Delete shadow instance (no active instance)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (no active instance) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_3_tests = [
ValidatingTest("Delete shadow instance (no active instance)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (no active instance) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
]
delete_4_tests = [
# Delete an inactive shadow instance with an active instance
Test("Delete shadow instance (mismatch)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (mismatch) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_5_tests = [
ValidatingTest("Delete shadow instance (mismatch)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (mismatch) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
# Delete an active shadow instance whose shadow file is missing
Test("Delete shadow instance (nonexistent shadow file)",
"crm_shadow --delete nonexistent_shadow",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (nonexistent shadow file) (force)",
"crm_shadow --delete nonexistent_shadow --force"),
ValidatingTest("Delete shadow instance (nonexistent shadow file)",
"crm_shadow --delete nonexistent_shadow --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (nonexistent shadow file) (force)",
"crm_shadow --delete nonexistent_shadow --force --output-as=xml"),
]
delete_6_tests = [
# Delete an active shadow instance when the CIB file is missing
Test("Delete shadow instance (nonexistent CIB file)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_7_tests = [
ValidatingTest("Delete shadow instance (nonexistent CIB file)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
]
create_1_tests = [
# Create new shadow instance based on active CIB with no instance active
make_test_group("Create copied shadow instance (no active instance)",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Create new shadow instance based on active CIB with other instance active
make_test_group("Create copied shadow instance (mismatch)",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Create new shadow instance based on CIB (shadow file already exists)
make_test_group("Create copied shadow instance (file already exists)",
f"crm_shadow --create {SHADOW_NAME} --batch",
expected_rc=ExitStatus.CANTCREAT),
make_test_group("Create copied shadow instance (file already exists) (force)",
f"crm_shadow --create {SHADOW_NAME} --batch --force"),
# Create new shadow instance based on active CIB when the CIB file is missing
make_test_group("Create copied shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --create {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH,
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
]
create_2_tests = [
# Create new empty shadow instance
make_test_group("Create empty shadow instance",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
# Create empty shadow instance with no active instance
make_test_group("Create empty shadow instance (no active instance)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Create empty shadow instance with other instance active
make_test_group("Create empty shadow instance (mismatch)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Create empty shadow instance when the CIB file is missing
make_test_group("Create empty shadow instance (nonexistent CIB file)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
# Create empty shadow instance (shadow file already exists)
make_test_group("Create empty shadow instance (file already exists)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
expected_rc=ExitStatus.CANTCREAT),
make_test_group("Create empty shadow instance (file already exists) (force)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch --force"),
# Query shadow instance with an empty CIB.
# --which and --file queries were done earlier.
TestGroup([
make_test_group("Get active shadow instance's contents (empty CIB)",
"crm_shadow --display"),
make_test_group("Get active shadow instance's diff (empty CIB)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
], setup=delete_shadow_resource_defaults),
]
reset_1_tests = [
Test("Resetting active shadow instance to active CIB requires force",
f"crm_shadow --reset {SHADOW_NAME} --batch",
expected_rc=ExitStatus.USAGE),
Test("Reset active shadow instance to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force"),
Test("Active shadow instance no different from active CIB after reset",
"crm_shadow --diff"),
Test("Active shadow instance differs from active CIB after change",
"crm_shadow --diff",
setup="crm_attribute -n admin_epoch -v 99",
expected_rc=ExitStatus.ERROR),
ValidatingTest("Reset active shadow instance to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force --output-as=xml"),
ValidatingTest("Active shadow instance no different from active CIB after reset",
"crm_shadow --diff --output-as=xml"),
ValidatingTest("Active shadow instance differs from active CIB after change",
"crm_shadow --diff --output-as=xml",
setup="crm_attribute -n admin_epoch -v 199",
expected_rc=ExitStatus.ERROR),
make_test_group("Reset shadow instance to active CIB with nonexistent shadow file",
f"crm_shadow --reset {SHADOW_NAME} --batch --force",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
Test("Active shadow instance no different from active CIB after force-reset",
"crm_shadow --diff"),
]
reset_2_tests = [
make_test_group("Reset inactive shadow instance (none active) to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --force --batch"),
]
reset_3_tests = [
make_test_group("Reset inactive shadow instance while another instance active",
f"crm_shadow --reset {SHADOW_NAME} --batch --force"),
]
reset_4_tests = [
make_test_group("Reset shadow instance with nonexistent CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH),
]
# Switch shadow instances
switch_tests = [
make_test_group("Switch to new shadow instance",
f"crm_shadow --switch {SHADOW_NAME} --batch"),
TestGroup([
make_test_group("Switch to nonexistent shadow instance",
f"crm_shadow --switch {SHADOW_NAME} --batch",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Switch to nonexistent shadow instance (force)",
f"crm_shadow --switch {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH),
], setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
]
return no_instance_tests + [
ShadowTestGroup(new_instance_tests + more_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(delete_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ShadowTestGroup(delete_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(delete_3_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(delete_4_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(delete_5_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(delete_6_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(delete_7_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(create_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(create_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(reset_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ShadowTestGroup(reset_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(reset_3_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(reset_4_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(switch_tests,
env={"CIB_shadow": "nonexistent_shadow"},
create_empty=True),
]
class CrmVerifyRegressionTest(RegressionTest):
"""A class for testing crm_verify."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_verify"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
invalid_tests = [
make_test_group("Verify a file-specified invalid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml",
expected_rc=ExitStatus.CONFIG),
make_test_group("Verify a file-specified invalid configuration (verbose)",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --verbose",
expected_rc=ExitStatus.CONFIG),
make_test_group("Verify a file-specified invalid configuration (quiet)",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --quiet",
expected_rc=ExitStatus.CONFIG),
ValidatingTest("Verify another file-specified invalid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_no_stonith.xml --output-as=xml",
expected_rc=ExitStatus.CONFIG),
]
with open(f"{test_home}/cli/crm_mon.xml", encoding="utf-8") as f:
cib_contents = f.read()
valid_tests = [
ValidatingTest("Verify a file-specified valid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml"),
ValidatingTest("Verify a piped-in valid configuration",
"crm_verify -p --output-as=xml",
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
ValidatingTest("Verbosely verify a file-specified valid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml --verbose"),
ValidatingTest("Verbosely verify a piped-in valid configuration",
"crm_verify -p --output-as=xml --verbose",
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
ValidatingTest("Verify a string-supplied valid configuration",
f"crm_verify -X '{cib_contents}' --output-as=xml"),
ValidatingTest("Verbosely verify a string-supplied valid configuration",
f"crm_verify -X '{cib_contents}' --output-as=xml --verbose"),
]
return invalid_tests + valid_tests
class CrmSimulateRegressionTest(RegressionTest):
"""A class for testing crm_simulate."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_simulate"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
good_cib = """
"""
bad_cib = good_cib.replace("start", "break")
bad_version_cib = good_cib.replace("pacemaker-1.2", "pacemaker-9999.0")
recoverable_cib = good_cib.replace("", "")
no_version_cib = good_cib.replace('validate-with="pacemaker-1.2" ', "")
no_version_bad_cib = bad_version_cib.replace('epoch="3"', 'epoch="30"').replace("start", "break")
basic_tests = [
Test("Show allocation scores with crm_simulate",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml --show-scores --output-as=xml"),
Test("Show utilization with crm_simulate",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml --show-utilization"),
Test("Simulate injecting a failure",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S -i ping_monitor_10000@cluster02=1"),
Test("Simulate bringing a node down",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S --node-down=cluster01"),
Test("Simulate a node failing",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S --node-fail=cluster02"),
Test("Run crm_simulate with invalid CIB (enum violation)",
"crm_simulate -p -S",
stdin=bad_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid CIB (unrecognized validate-with)",
"crm_simulate -p -S",
stdin=bad_version_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1)",
"crm_simulate -p -S",
stdin=recoverable_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"}),
Test("Run crm_simulate with valid CIB, but without validate-with attribute",
"crm_simulate -p -S",
stdin=no_version_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid CIB, also without validate-with attribute",
"crm_simulate -p -S",
stdin=no_version_bad_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
]
return [
ShadowTestGroup(basic_tests, create=False,
env={"CIB_shadow": None}),
]
class CrmDiffRegressionTest(RegressionTest):
"""A class for testing crm_diff."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_diff"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
old_file = f"{cts_cli_data}/crm_diff_old.xml"
new_file = f"{cts_cli_data}/crm_diff_new.xml"
patch_file = f"{cts_cli_data}/crm_diff_patchset.xml"
cib_patch_file = f"{cts_cli_data}/crm_diff_patchset_cib.xml"
# Enclose the strings in quotes now rather than in the command lines
with open(f"{cts_cli_data}/crm_diff_old.xml", "r") as file:
old_str = f"'{file.read()}'"
with open(f"{cts_cli_data}/crm_diff_new.xml", "r") as file:
new_str = f"'{file.read()}'"
return [
make_test_group("Create an XML patchset from files",
f"crm_diff -o {old_file} -n {new_file}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset from strings",
f"crm_diff -O {old_str} -N {new_str}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset from old file, new string",
f"crm_diff -o {old_file} -N {new_str}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset from old string, new file",
f"crm_diff -O {old_str} -n {new_file}",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset as CIB",
f"crm_diff -o {old_file} -n {new_file} --cib",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset with no versions",
f"crm_diff -o {old_file} -n {new_file} --no-version",
expected_rc=ExitStatus.ERROR),
make_test_group("Create an XML patchset as CIB, with no versions",
f"crm_diff -o {old_file} -n {new_file} --cib --no-version",
expected_rc=ExitStatus.USAGE),
# Patch must be a file (cannot be a string).
#
# patch_file was generated using the following command:
#
# # crm_diff -o {old_file} -n {new_file}
#
make_test_group("Apply an XML patchset to a file",
f"crm_diff -o {old_file} -p {patch_file}"),
make_test_group("Apply an XML patchset to a string",
f"crm_diff -O {old_str} -p {patch_file}"),
make_test_group("Apply an XML patchset as CIB",
f"crm_diff -o {old_file} -p {patch_file} --cib"),
make_test_group("Apply an XML patchset with no versions",
f"crm_diff -o {old_file} -p {patch_file} --no-version"),
make_test_group("Apply an XML patchset as CIB, with no versions",
f"crm_diff -o {old_file} -p {patch_file} --cib --no-version",
expected_rc=ExitStatus.USAGE),
# cib_patch_file was generated using the following command:
#
# # crm_diff -o {old_file} -n {new_file} --cib
#
# Thus a digest was added to the patchset, and attribute position
# changes were ignored.
#
# @FIXME Currently these all fail due to digest mismatch. The issue
# goes back to at least Pacemaker 1.1.24. However, note that they
# fail with a generic error code, not a digest error code.
#
# It seems reasonable that a patchset generated by crm_diff should
# possible to apply to the old XML using crm_diff.
make_test_group("Apply an XML patchset generated as CIB",
f"crm_diff -o {old_file} -p {cib_patch_file}",
expected_rc=ExitStatus.ERROR),
make_test_group("Apply an XML patchset generated as CIB, as CIB",
f"crm_diff -o {old_file} -p {cib_patch_file} --cib",
expected_rc=ExitStatus.ERROR),
make_test_group("Apply an XML patchset generated as CIB, with no versions",
f"crm_diff -o {old_file} -p {cib_patch_file} --no-version",
expected_rc=ExitStatus.ERROR),
# @TODO We could add tests where the old and new CIBs have the same
# version info. In that case, at the time of writing, generating a
# patchset with --cib and then trying to apply it will result in
# ExitStatus.OLD.
]
class CrmMonRegressionTest(RegressionTest):
"""A class for testing crm_mon."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_mon"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
make_test_group("Basic output", "crm_mon -1"),
make_test_group("Output without node section",
"crm_mon -1 --exclude=nodes"),
# The next test doesn't need to be performed for other output formats. It's
# really just a test to make sure that blank lines are correct.
Test("Output with only the node section",
"crm_mon -1 --exclude=all --include=nodes"),
# XML includes everything already so there's no need for a complete test
Test("Complete text output", "crm_mon -1 --include=all"),
# XML includes detailed output already
Test("Complete text output with detail", "crm_mon -1R --include=all"),
Test("Complete brief text output", "crm_mon -1 --include=all --brief"),
Test("Complete text output grouped by node",
"crm_mon -1 --include=all --group-by-node"),
# XML does not have a brief output option
Test("Complete brief text output grouped by node",
"crm_mon -1 --include=all --group-by-node --brief"),
ValidatingTest("Output grouped by node",
"crm_mon --output-as=xml --group-by-node"),
make_test_group("Complete output filtered by node",
"crm_mon -1 --include=all --node=cluster01"),
make_test_group("Complete output filtered by tag",
"crm_mon -1 --include=all --node=even-nodes"),
make_test_group("Complete output filtered by resource tag",
"crm_mon -1 --include=all --resource=fencing-rscs"),
make_test_group("Output filtered by node that doesn't exist",
"crm_mon -1 --node=blah"),
Test("Basic text output with inactive resources", "crm_mon -1 -r"),
# XML already includes inactive resources
Test("Basic text output with inactive resources, filtered by node",
"crm_mon -1 -r --node=cluster02"),
make_test_group("Complete output filtered by primitive resource",
"crm_mon -1 --include=all --resource=Fencing"),
make_test_group("Complete output filtered by group resource",
"crm_mon -1 --include=all --resource=exim-group"),
Test("Complete text output filtered by group resource member",
"crm_mon -1 --include=all --resource=Public-IP"),
ValidatingTest("Output filtered by group resource member",
"crm_mon --output-as=xml --resource=Email"),
make_test_group("Complete output filtered by clone resource",
"crm_mon -1 --include=all --resource=ping-clone"),
make_test_group("Complete output filtered by clone resource instance",
"crm_mon -1 --include=all --resource=ping"),
Test("Complete text output filtered by exact clone resource instance",
"crm_mon -1 --include=all --show-detail --resource=ping:0"),
ValidatingTest("Output filtered by exact clone resource instance",
"crm_mon --output-as=xml --resource=ping:1"),
make_test_group("Output filtered by resource that doesn't exist",
"crm_mon -1 --resource=blah"),
Test("Basic text output with inactive resources, filtered by tag",
"crm_mon -1 -r --resource=inactive-rscs"),
Test("Basic text output with inactive resources, filtered by bundle resource",
"crm_mon -1 -r --resource=httpd-bundle"),
ValidatingTest("Output filtered by inactive bundle resource",
"crm_mon --output-as=xml --resource=httpd-bundle"),
Test("Basic text output with inactive resources, filtered by bundled IP address resource",
"crm_mon -1 -r --resource=httpd-bundle-ip-192.168.122.131"),
ValidatingTest("Output filtered by bundled IP address resource",
"crm_mon --output-as=xml --resource=httpd-bundle-ip-192.168.122.132"),
Test("Basic text output with inactive resources, filtered by bundled container",
"crm_mon -1 -r --resource=httpd-bundle-docker-1"),
ValidatingTest("Output filtered by bundled container",
"crm_mon --output-as=xml --resource=httpd-bundle-docker-2"),
Test("Basic text output with inactive resources, filtered by bundle connection",
"crm_mon -1 -r --resource=httpd-bundle-0"),
ValidatingTest("Output filtered by bundle connection",
"crm_mon --output-as=xml --resource=httpd-bundle-0"),
Test("Basic text output with inactive resources, filtered by bundled primitive resource",
"crm_mon -1 -r --resource=httpd"),
ValidatingTest("Output filtered by bundled primitive resource",
"crm_mon --output-as=xml --resource=httpd"),
Test("Complete text output, filtered by clone name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-clone-group"),
ValidatingTest("Output, filtered by clone name in cloned group",
"crm_mon --output-as=xml --resource=mysql-clone-group"),
Test("Complete text output, filtered by group name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-group"),
ValidatingTest("Output, filtered by group name in cloned group",
"crm_mon --output-as=xml --resource=mysql-group"),
Test("Complete text output, filtered by exact group instance name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-group:1"),
ValidatingTest("Output, filtered by exact group instance name in cloned group",
"crm_mon --output-as=xml --resource=mysql-group:1"),
Test("Complete text output, filtered by primitive name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-proxy"),
ValidatingTest("Output, filtered by primitive name in cloned group",
"crm_mon --output-as=xml --resource=mysql-proxy"),
Test("Complete text output, filtered by exact primitive instance name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-proxy:1"),
ValidatingTest("Output, filtered by exact primitive instance name in cloned group",
"crm_mon --output-as=xml --resource=mysql-proxy:1"),
]
partial_tests = [
Test("Output of partially active resources", "crm_mon -1 --show-detail"),
ValidatingTest("Output of partially active resources", "crm_mon --output-as=xml"),
Test("Output of partially active resources, with inactive resources",
"crm_mon -1 -r --show-detail"),
# XML already includes inactive resources
Test("Complete brief text output, with inactive resources",
"crm_mon -1 -r --include=all --brief --show-detail"),
# XML does not have a brief output option
Test("Text output of partially active group", "crm_mon -1 --resource=partially-active-group"),
Test("Text output of partially active group, with inactive resources",
"crm_mon -1 --resource=partially-active-group -r"),
Test("Text output of active member of partially active group",
"crm_mon -1 --resource=dummy-1"),
Test("Text output of inactive member of partially active group",
"crm_mon -1 --resource=dummy-2 --show-detail"),
Test("Complete brief text output grouped by node, with inactive resources",
"crm_mon -1 -r --include=all --group-by-node --brief --show-detail"),
Test("Text output of partially active resources, with inactive resources, filtered by node",
"crm_mon -1 -r --node=cluster01"),
ValidatingTest("Output of partially active resources, filtered by node",
"crm_mon --output-as=xml --node=cluster01"),
]
unmanaged_tests = [
make_test_group("Output of active unmanaged resource on offline node",
"crm_mon -1"),
Test("Brief text output of active unmanaged resource on offline node",
"crm_mon -1 --brief"),
Test("Brief text output of active unmanaged resource on offline node, grouped by node",
"crm_mon -1 --brief --group-by-node"),
]
maint1_tests = [
make_test_group("Output of all resources with maintenance-mode enabled",
"crm_mon -1 -r",
setup="crm_attribute -n maintenance-mode -v true",
teardown="crm_attribute -n maintenance-mode -v false"),
make_test_group("Output of all resources with maintenance enabled for a node",
"crm_mon -1 -r",
setup="crm_attribute -n maintenance -N cluster02 -v true",
teardown="crm_attribute -n maintenance -N cluster02 -v false"),
]
maint2_tests = [
# The fence resource is excluded, for comparison
make_test_group("Output of all resources with maintenance meta attribute true",
"crm_mon -1 -r"),
]
t180_tests = [
Test("Text output of guest node's container on different node from its remote resource",
"crm_mon -1"),
Test("Complete text output of guest node's container on different node from its remote resource",
"crm_mon -1 --show-detail"),
]
return [
TestGroup(basic_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
Test("Check that CIB_file=\"-\" works", "crm_mon -1",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
TestGroup(partial_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-partial.xml"}),
TestGroup(unmanaged_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-unmanaged.xml"}),
TestGroup(maint1_tests,
cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
TestGroup(maint2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-rsc-maint.xml"}),
TestGroup(t180_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-T180.xml"}),
]
class AclsRegressionTest(RegressionTest):
"""A class for testing access control lists."""
@property
def name(self):
"""Return the name of this regression test."""
return "acls"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
acl_cib = """
"""
basic_tests = [
Test("Configure some ACLs", "cibadmin -M -o acls -p",
update_cib=True, stdin=acl_cib),
Test("Enable ACLs", "crm_attribute -n enable-acl -v true",
update_cib=True),
Test("Set cluster option", "crm_attribute -n no-quorum-policy -v ignore",
update_cib=True),
Test("New ACL role",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("New ACL target",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Another ACL role",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Another ACL target",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Updated ACL",
"""cibadmin --replace -o acls --xml-text ''""",
update_cib=True),
]
no_acl_tests = [
Test("unknownguy: Query configuration", "cibadmin -Q",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
deny_cib_tests = [
Test("l33t-haxor: Query configuration",
"cibadmin -Q",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
observer_tests = [
Test("niceguy: Query configuration", "cibadmin -Q"),
Test("niceguy: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("niceguy: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
update_cib=True),
Test("niceguy: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("root: Query configuration", "cibadmin -Q",
env={"CIB_user": "root"}),
Test("root: Set stonith-enabled", "crm_attribute -n stonith-enabled -v true",
update_cib=True, env={"CIB_user": "root"}),
Test("root: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True, env={"CIB_user": "root"}),
# For use with later tests
Test("root: Create another resource (with description)",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True, env={"CIB_user": "root"}),
]
deny_cib_2_tests = [
Test("l33t-haxor: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Query a resource meta attribute",
"crm_resource -r dummy --meta -g target-role",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Remove a resource meta attribute",
"crm_resource -r dummy --meta -d target-role",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
observer_2_tests = [
Test("niceguy: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped",
update_cib=True),
Test("niceguy: Query a resource meta attribute",
"crm_resource -r dummy --meta -g target-role",
update_cib=True),
Test("niceguy: Remove a resource meta attribute",
"crm_resource -r dummy --meta -d target-role",
update_cib=True),
Test("niceguy: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Started",
update_cib=True),
]
read_meta_tests = [
Test("badidea: Query configuration - implied deny", "cibadmin -Q"),
]
deny_cib_3_tests = [
Test("betteridea: Query configuration - explicit deny", "cibadmin -Q"),
]
replace_tests = [
TestGroup([
AclTest("niceguy: Replace - remove acls",
"cibadmin --replace -p",
setup="cibadmin --delete --xml-text ''",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - create resource",
"cibadmin --replace -p",
setup="""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - modify attribute (deny)",
"cibadmin --replace -p",
setup="crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - delete attribute (deny)",
"cibadmin --replace -p",
setup="""cibadmin --replace --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - create attribute (deny)",
"cibadmin --replace -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
], env={"CIB_user": "niceguy"}),
# admin role
TestGroup([
AclTest("bob: Replace - create attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("bob: Replace - modify attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("bob: Replace - delete attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
], env={"CIB_user": "bob"}),
# super_user role
TestGroup([
AclTest("joe: Replace - create attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("joe: Replace - modify attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("joe: Replace - delete attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
], env={"CIB_user": "joe"}),
# rsc_writer role
TestGroup([
AclTest("mike: Replace - create attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("mike: Replace - modify attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("mike: Replace - delete attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
# Create an additional resource for deny-overrides-allow testing
AclTest("mike: Create another resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
], env={"CIB_user": "mike"}),
# rsc_denied role
TestGroup([
AclTest("chris: Replace - create attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("chris: Replace - modify attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("chris: Replace - delete attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
], env={"CIB_user": "chris"}),
]
loop_tests = [
# no ACL
TestGroup(no_acl_tests, env={"CIB_user": "unknownguy"}),
# deny /cib permission
TestGroup(deny_cib_tests, env={"CIB_user": "l33t-haxor"}),
# observer role
TestGroup(observer_tests, env={"CIB_user": "niceguy"}),
# deny /cib permission
TestGroup(deny_cib_2_tests, env={"CIB_user": "l33t-haxor"}),
# observer role
TestGroup(observer_2_tests, env={"CIB_user": "niceguy"}),
# read //meta_attributes
TestGroup(read_meta_tests, env={"CIB_user": "badidea"}),
# deny /cib, read //meta_attributes
TestGroup(deny_cib_3_tests, env={"CIB_user": "betteridea"}),
] + replace_tests
return [
ShadowTestGroup(basic_tests + [
TestGroup(loop_tests,
env={"PCMK_trace_functions": "pcmk__check_acl,pcmk__apply_creation_acl"})]),
]
class ValidityRegressionTest(RegressionTest):
"""A class for testing CIB validity."""
@property
def name(self):
"""Return the name of this regression test."""
return "validity"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
# sanitize_output() strips out validate-with, so there's no point in
# outputting the CIB after tests that modify it
Test("Try to set unrecognized validate-with",
"cibadmin -M --xml-text ''",
expected_rc=ExitStatus.CONFIG),
Test("Try to remove validate-with attribute",
"cibadmin -R -p",
stdin=StdinCmd("""cibadmin -Q | sed 's#validate-with="[^"]*"##'"""),
expected_rc=ExitStatus.CONFIG),
Test("Try to use rsc_order first-action value disallowed by schema",
"cibadmin -M -o constraints --xml-text ''",
expected_rc=ExitStatus.CONFIG, update_cib=True),
Test("Try to use configuration legal only with schema after configured one",
"cibadmin -C -o configuration --xml-text ''",
expected_rc=ExitStatus.CONFIG, update_cib=True),
Test("Disable schema validation",
"cibadmin -M --xml-text ''",
expected_rc=ExitStatus.OK),
Test("Set invalid rsc_order first-action value (schema validation disabled)",
"cibadmin -M -o constraints --xml-text ''",
expected_rc=ExitStatus.OK, update_cib=True),
Test("Run crm_simulate with invalid rsc_order first-action "
"(schema validation disabled)",
"crm_simulate -SL",
expected_rc=ExitStatus.OK),
]
basic_tests_setup = [
"""cibadmin -C -o resources --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''""",
"""cibadmin -C -o constraints --xml-text ''""",
]
return [
ShadowTestGroup(basic_tests, validate_with="pacemaker-1.2",
setup=basic_tests_setup,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema,invert_action"}),
]
class UpgradeRegressionTest(RegressionTest):
"""A class for testing upgrading the CIB."""
@property
def name(self):
"""Return the name of this regression test."""
return "upgrade"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
resource_cib = """
"""
basic_tests = [
Test("Set stonith-enabled=false", "crm_attribute -n stonith-enabled -v false",
update_cib=True),
Test("Configure the initial resource", "cibadmin -M -o resources -p",
update_cib=True, stdin=resource_cib),
Test("Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)",
"cibadmin --upgrade --force -V -V",
update_cib=True),
Test("Query a resource instance attribute (shall survive)",
"crm_resource -r mySmartFuse -g requires",
update_cib=True),
]
return [
ShadowTestGroup(basic_tests, validate_with="pacemaker-2.10",
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"})
]
class RulesRegressionTest(RegressionTest):
"""A class for testing support for CIB rules."""
@property
def name(self):
"""Return the name of this regression test."""
return "rules"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
tomorrow = datetime.now() + timedelta(days=1)
rule_cib = f"""
"""
usage_tests = [
make_test_group("crm_rule given no arguments", "crm_rule",
expected_rc=ExitStatus.USAGE),
make_test_group("crm_rule given no rule to check", "crm_rule -c",
expected_rc=ExitStatus.USAGE),
make_test_group("crm_rule given invalid input XML",
"crm_rule -c -r blahblah -X invalidxml",
expected_rc=ExitStatus.DATAERR),
make_test_group("crm_rule given invalid input XML on stdin",
"crm_rule -c -r blahblah -X -",
stdin=StdinCmd("echo invalidxml"),
expected_rc=ExitStatus.DATAERR),
]
basic_tests = [
make_test_group("Try to check a rule that doesn't exist",
"crm_rule -c -r blahblah",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Try to check a rule that has too many date_expressions",
"crm_rule -c -r cli-rule-too-many-date-expressions",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
make_test_group("Verify basic rule is expired",
"crm_rule -c -r cli-prefer-rule-dummy-expired",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify basic rule worked in the past",
"crm_rule -c -r cli-prefer-rule-dummy-expired -d 20180101"),
make_test_group("Verify basic rule is not yet in effect",
"crm_rule -c -r cli-prefer-rule-dummy-not-yet",
expected_rc=ExitStatus.NOT_YET_IN_EFFECT),
make_test_group("Verify date_spec rule with years has expired",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify multiple rules at once",
"crm_rule -c -r cli-prefer-rule-dummy-not-yet -r cli-prefer-rule-dummy-date_spec-only-years",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify date_spec rule with years is in effect",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years -d 20190201"),
make_test_group("Try to check a rule whose date_spec does not contain years=",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-without-years",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
make_test_group("Try to check a rule with no date_expression",
"crm_rule -c -r cli-no-date_expression-rule",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
]
return usage_tests + [
TestGroup(basic_tests, cib_gen=partial(write_cib, rule_cib))
]
class FeatureSetRegressionTest(RegressionTest):
"""A class for testing support for version-specific features."""
@property
def name(self):
"""Return the name of this regression test."""
return "feature_set"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
# Import the test CIB
Test("Import the test CIB",
f"cibadmin --replace --xml-file {cts_cli_data}/crm_mon-feature_set.xml",
update_cib=True),
Test("Complete text output, no mixed status",
"crm_mon -1 --show-detail"),
ValidatingTest("Output, no mixed status", "crm_mon --output-as=xml"),
# Modify the CIB to fake that the cluster has mixed versions
Test("Fake inconsistent feature set",
"crm_attribute --node=cluster02 --name=#feature-set --update=3.15.0 --lifetime=reboot",
update_cib=True),
Test("Complete text output, mixed status",
"crm_mon -1 --show-detail"),
ValidatingTest("Output, mixed status", "crm_mon --output-as=xml"),
]
return [
ShadowTestGroup(basic_tests),
]
# Tests that depend on resource agents and must be run in an installed
# environment
class AgentRegressionTest(RegressionTest):
"""A class for testing resource agents."""
@property
def name(self):
"""Return the name of this regression test."""
return "agents"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
return [
make_test_group("Validate a valid resource configuration",
"crm_resource --validate --class ocf --provider pacemaker --agent Dummy"),
# Make the Dummy configuration invalid (op_sleep can't be a generic string)
make_test_group("Validate an invalid resource configuration",
"crm_resource --validate --class ocf --provider pacemaker --agent Dummy",
expected_rc=ExitStatus.NOT_CONFIGURED,
env={"OCF_RESKEY_op_sleep": "asdf"}),
]
def build_options():
"""Handle command line arguments."""
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
description="Command line tool regression tests",
epilog=f"Default tests: {' '.join(default_tests)}\n"
"Other tests: agents (must be run in an installed environment)")
parser.add_argument("-j", "--jobs", metavar="JOBS", default=cpu_count() - 1, type=int,
help="The number of tests to run simultaneously")
parser.add_argument("-p", "--path", metavar="DIR", action="append",
help="Look for executables in DIR (may be specified multiple times)")
parser.add_argument("-r", "--run-only", metavar="TEST", choices=default_tests + ["tools"] + other_tests,
action="append",
help="Run only specified tests (may be specified multiple times)")
parser.add_argument("-s", "--save", action="store_true",
help="Save actual output as expected output")
parser.add_argument("-v", "--valgrind", action="store_true",
help="Run all commands under valgrind")
parser.add_argument("-V", "--verbose", action="store_true",
help="Display any differences from expected output")
args = parser.parse_args()
if args.path is None:
args.path = []
return args
def setup_environment(valgrind):
"""Set various environment variables needed for operation."""
if valgrind:
os.environ["G_SLICE"] = "always-malloc"
# Ensure all command output is in portable locale for comparison
os.environ["LC_ALL"] = "C"
# Log test errors to stderr
os.environ["PCMK_stderr"] = "1"
# Because we will change the value of PCMK_trace_functions and then reset it
# back to some initial value at various points, it's easiest to assume it is
# defined but empty by default
if "PCMK_trace_functions" not in os.environ:
os.environ["PCMK_trace_functions"] = ""
def path_prepend(p):
"""Add another directory to the front of $PATH."""
old = os.environ["PATH"]
os.environ["PATH"] = f"{p}:{old}"
def setup_path(opts_path):
"""Set the PATH environment variable appropriately for the tests."""
srcdir = os.path.dirname(test_home)
# Add any search paths given on the command line
for p in opts_path:
path_prepend(p)
if os.path.exists(f"{srcdir}/tools/crm_simulate"):
print(f"Using local binaries from: {srcdir}")
path_prepend(f"{srcdir}/tools")
for daemon in ["based", "controld", "fenced", "schedulerd"]:
path_prepend(f"{srcdir}/daemons/{daemon}")
print(f"Using local schemas from: {srcdir}/xml")
os.environ["PCMK_schema_directory"] = f"{srcdir}/xml"
else:
path_prepend(BuildOptions.DAEMON_DIR)
os.environ["PCMK_schema_directory"] = BuildOptions.SCHEMA_DIR
def _run_one(valgrind, r):
"""Run and return a TestGroup object."""
# See comments in run_regression_tests.
r.run(valgrind=valgrind)
return r
def run_regression_tests(regs, jobs, valgrind=False):
"""Run the given tests and return the modified objects."""
executed = []
with Pool(processes=jobs) as pool:
# What we really want to do here is:
# pool.map(lambda r: r.run(),regs)
#
# However, multiprocessing uses pickle somehow in its operation, and python
# doesn't want to pickle a lambda (nor a nested function within this one).
# Thus, we need to use the _run_one wrapper at the file level just to call
# run(). Further, if we don't return the modified object from that and then
# return the list of modified objects here, it looks like the rest of the
# program will use the originals, before this was ever run.
executed = pool.map(partial(_run_one, valgrind), regs)
return executed
def results(regs, save, verbose):
"""Print the output from each regression test, returning the number whose output differs."""
output_differs = 0
if verbose:
print("\n\nResults")
sys.stdout.flush()
for r in regs:
r.write()
if save:
dest = f"{test_home}/cli/regression.{r.name}.exp"
copyfile(r.results_file, dest)
r.diff(verbose)
if not r.identical:
output_differs += 1
return output_differs
def summary(regs, output_differs, verbose):
"""Print the summary output for the entire test run."""
test_failures = 0
test_successes = 0
for r in regs:
test_failures += r.failures
test_successes += r.successes
print("\n\nSummary")
sys.stdout.flush()
# First, print all the Passed/Failed lines from each Test run.
for r in regs:
print("\n".join(r.summary))
fmt = PluralFormatter()
# Then, print information specific to each result possibility. Basically,
# if there were failures then we print the output differences, leave the
# failed output files in place, and exit with an error. Otherwise, clean up
# anything that passed.
if test_failures > 0 and output_differs > 0:
print(fmt.format("{0} {0:plural,test} failed; see output in:",
test_failures))
for r in regs:
r.process_results(verbose)
return ExitStatus.ERROR
if test_failures > 0:
print(fmt.format("{0} {0:plural,test} failed", test_failures))
for r in regs:
r.process_results(verbose)
return ExitStatus.ERROR
if output_differs:
print(fmt.format("{0} {0:plural,test} passed but output was "
"unexpected; see output in:", test_successes))
for r in regs:
r.process_results(verbose)
return ExitStatus.DIGEST
print(fmt.format("{0} {0:plural,test} passed", test_successes))
for r in regs:
r.cleanup()
return ExitStatus.OK
regression_classes = [
AccessRenderRegressionTest,
DaemonsRegressionTest,
DatesRegressionTest,
ErrorCodeRegressionTest,
CibadminRegressionTest,
CrmAttributeRegressionTest,
CrmStandbyRegressionTest,
CrmResourceRegressionTest,
CrmTicketRegressionTest,
CrmadminRegressionTest,
CrmShadowRegressionTest,
CrmVerifyRegressionTest,
CrmSimulateRegressionTest,
CrmDiffRegressionTest,
CrmMonRegressionTest,
AclsRegressionTest,
ValidityRegressionTest,
UpgradeRegressionTest,
RulesRegressionTest,
FeatureSetRegressionTest,
AgentRegressionTest,
]
def main():
"""Run command line regression tests as specified by arguments."""
opts = build_options()
setup_environment(opts.valgrind)
setup_path(opts.path)
# Filter the list of all regression test classes to include only those that
# were requested on the command line. If empty, this defaults to default_tests.
if not opts.run_only:
opts.run_only = default_tests
if opts.run_only == ["tools"]:
opts.run_only = tools_tests
regs = []
for cls in regression_classes:
obj = cls()
if obj.name in opts.run_only:
regs.append(obj)
regs = run_regression_tests(regs, max(1, opts.jobs), valgrind=opts.valgrind)
output_differs = results(regs, opts.save, opts.verbose)
rc = summary(regs, output_differs, opts.verbose)
sys.exit(rc)
if __name__ == "__main__":
main()
# vim: set filetype=python:
diff --git a/include/crm/cib.h b/include/crm/cib.h
index 8fcc257d08..87ec5f3c55 100644
--- a/include/crm/cib.h
+++ b/include/crm/cib.h
@@ -1,66 +1,65 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_CIB__H
# define PCMK__CRM_CIB__H
# include // gboolean
# include
# include
# include
# include
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Cluster Configuration
* \ingroup cib
*/
// Use compare_version() for doing comparisons
# define CIB_FEATURE_SET "2.0"
/* Core functions */
// NOTE: sbd (as of at least 1.5.2) uses this
cib_t *cib_new(void);
cib_t *cib_native_new(void);
cib_t *cib_file_new(const char *filename);
cib_t *cib_remote_new(const char *server, const char *user, const char *passwd, int port,
gboolean encrypted);
-cib_t *cib_new_no_shadow(void);
char *get_shadow_file(const char *name);
cib_t *cib_shadow_new(const char *name);
void cib_free_notify(cib_t *cib);
void cib_free_callbacks(cib_t *cib);
// NOTE: sbd (as of at least 1.5.2) uses this
void cib_delete(cib_t * cib);
void cib_dump_pending_callbacks(void);
int num_cib_op_callbacks(void);
void remove_cib_op_callback(int call_id, gboolean all_callbacks);
#define CIB_LIBRARY "libcib.so.54"
#ifdef __cplusplus
}
#endif
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include
#endif
#endif
diff --git a/include/crm/cib/internal.h b/include/crm/cib/internal.h
index c06d8414b6..8e43cf88a0 100644
--- a/include/crm/cib/internal.h
+++ b/include/crm/cib/internal.h
@@ -1,316 +1,316 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_CIB_INTERNAL__H
#define PCMK__CRM_CIB_INTERNAL__H
#include
#include
#include
#include
#include
#ifdef __cplusplus
extern "C" {
#endif
// Request types for CIB manager IPC/CPG
#define PCMK__CIB_REQUEST_SECONDARY "cib_slave"
#define PCMK__CIB_REQUEST_PRIMARY "cib_master"
#define PCMK__CIB_REQUEST_SYNC_TO_ALL "cib_sync"
#define PCMK__CIB_REQUEST_SYNC_TO_ONE "cib_sync_one"
#define PCMK__CIB_REQUEST_IS_PRIMARY "cib_ismaster"
#define PCMK__CIB_REQUEST_BUMP "cib_bump"
#define PCMK__CIB_REQUEST_QUERY "cib_query"
#define PCMK__CIB_REQUEST_CREATE "cib_create"
#define PCMK__CIB_REQUEST_MODIFY "cib_modify"
#define PCMK__CIB_REQUEST_DELETE "cib_delete"
#define PCMK__CIB_REQUEST_ERASE "cib_erase"
#define PCMK__CIB_REQUEST_REPLACE "cib_replace"
#define PCMK__CIB_REQUEST_APPLY_PATCH "cib_apply_diff"
#define PCMK__CIB_REQUEST_UPGRADE "cib_upgrade"
#define PCMK__CIB_REQUEST_ABS_DELETE "cib_delete_alt"
#define PCMK__CIB_REQUEST_NOOP "noop"
#define PCMK__CIB_REQUEST_SHUTDOWN "cib_shutdown_req"
#define PCMK__CIB_REQUEST_COMMIT_TRANSACT "cib_commit_transact"
#define PCMK__CIB_REQUEST_SCHEMAS "cib_schemas"
/*!
* \internal
* \brief Flags for CIB operation attributes
*/
enum cib__op_attr {
cib__op_attr_none = 0, //!< No special attributes
cib__op_attr_modifies = (1 << 1), //!< Modifies CIB
cib__op_attr_privileged = (1 << 2), //!< Requires privileges
cib__op_attr_local = (1 << 3), //!< Must only be processed locally
cib__op_attr_replaces = (1 << 4), //!< Replaces CIB
cib__op_attr_writes_through = (1 << 5), //!< Writes to disk on success
cib__op_attr_transaction = (1 << 6), //!< Supported in a transaction
};
/*!
* \internal
* \brief Types of CIB operations
*/
enum cib__op_type {
cib__op_abs_delete,
cib__op_apply_patch,
cib__op_bump,
cib__op_commit_transact,
cib__op_create,
cib__op_delete,
cib__op_erase,
cib__op_is_primary,
cib__op_modify,
cib__op_noop,
cib__op_ping,
cib__op_primary,
cib__op_query,
cib__op_replace,
cib__op_secondary,
cib__op_shutdown,
cib__op_sync_all,
cib__op_sync_one,
cib__op_upgrade,
cib__op_schemas,
};
gboolean cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates,
int *_admin_epoch, int *_epoch, int *_updates);
gboolean cib_read_config(GHashTable * options, xmlNode * current_cib);
typedef int (*cib__op_fn_t)(const char *, int, const char *, xmlNode *,
xmlNode *, xmlNode *, xmlNode **, xmlNode **);
typedef struct cib__operation_s {
const char *name;
enum cib__op_type type;
uint32_t flags; //!< Group of enum cib__op_attr flags
} cib__operation_t;
typedef struct cib_notify_client_s {
const char *event;
const char *obj_id; /* implement one day */
const char *obj_type; /* implement one day */
void (*callback) (const char *event, xmlNode * msg);
} cib_notify_client_t;
typedef struct cib_callback_client_s {
void (*callback) (xmlNode *, int, int, xmlNode *, void *);
const char *id;
void *user_data;
gboolean only_success;
struct timer_rec_s *timer;
void (*free_func)(void *);
} cib_callback_client_t;
struct timer_rec_s {
int call_id;
int timeout;
guint ref;
cib_t *cib;
};
#define cib__set_call_options(cib_call_opts, call_for, flags_to_set) do { \
cib_call_opts = pcmk__set_flags_as(__func__, __LINE__, \
LOG_TRACE, "CIB call", (call_for), (cib_call_opts), \
(flags_to_set), #flags_to_set); \
} while (0)
#define cib__clear_call_options(cib_call_opts, call_for, flags_to_clear) do { \
cib_call_opts = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_TRACE, "CIB call", (call_for), (cib_call_opts), \
(flags_to_clear), #flags_to_clear); \
} while (0)
cib_t *cib_new_variant(void);
/*!
* \internal
* \brief Check whether a given CIB client's update should trigger a refresh
*
* Here, "refresh" means that Pacemaker daemons write out their current state.
*
* If a Pacemaker daemon or one of certain Pacemaker CLI tools modifies the CIB,
* we can assume that the CIB hasn't diverged from the true cluster state. A
* "safe" CLI tool requests that all relevant daemons update their state before
* the tool requests any CIB modifications directly.
*
* In contrast, other "unsafe" tools (for example, \c cibadmin and external
* tools) may request arbitrary CIB changes.
*
* A Pacemaker daemon can write out its current state to the CIB when it's
* notified of an update from an unsafe client, to ensure the CIB still contains
* the daemon's correct state.
*
* \param[in] name CIB client name
*
* \return \c true if the CIB client should trigger a refresh, or \c false
* otherwise
*/
static inline bool
cib__client_triggers_refresh(const char *name)
{
return (pcmk__parse_server(name) == pcmk_ipc_unknown)
&& !pcmk__str_any_of(name,
"attrd_updater",
"crm_attribute",
"crm_node",
"crm_resource",
"crm_ticket",
NULL);
}
int cib__get_notify_patchset(const xmlNode *msg, const xmlNode **patchset);
int cib_perform_op(cib_t *cib, const char *op, uint32_t call_options,
cib__op_fn_t fn, bool is_query, const char *section,
xmlNode *req, xmlNode *input, bool manage_counters,
bool *config_changed, xmlNode **current_cib,
xmlNode **result_cib, xmlNode **diff, xmlNode **output);
int cib__create_op(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data, int call_options,
const char *user_name, const char *client_name,
xmlNode **op_msg);
int cib__extend_transaction(cib_t *cib, xmlNode *request);
void cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc);
void cib_native_notify(gpointer data, gpointer user_data);
int cib__get_operation(const char *op, const cib__operation_t **operation);
int cib_process_query(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_erase(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_bump(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_replace(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_create(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_modify(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_delete(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_diff(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
int cib_process_upgrade(const char *op, int options, const char *section, xmlNode * req,
xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib,
xmlNode ** answer);
/*!
* \internal
* \brief Query or modify a CIB
*
* \param[in] op PCMK__CIB_REQUEST_* operation to be performed
* \param[in] options Flag set of \c cib_call_options
* \param[in] section XPath to query or modify
* \param[in] req unused
* \param[in] input Portion of CIB to modify (used with
* PCMK__CIB_REQUEST_CREATE,
* PCMK__CIB_REQUEST_MODIFY, and
* PCMK__CIB_REQUEST_REPLACE)
* \param[in,out] existing_cib Input CIB (used with PCMK__CIB_REQUEST_QUERY)
* \param[in,out] result_cib CIB copy to make changes in (used with
* PCMK__CIB_REQUEST_CREATE,
* PCMK__CIB_REQUEST_MODIFY,
* PCMK__CIB_REQUEST_DELETE, and
* PCMK__CIB_REQUEST_REPLACE)
* \param[out] answer Query result (used with PCMK__CIB_REQUEST_QUERY)
*
* \return Legacy Pacemaker return code
*/
int cib_process_xpath(const char *op, int options, const char *section,
const xmlNode *req, xmlNode *input, xmlNode *existing_cib,
xmlNode **result_cib, xmlNode ** answer);
int cib_internal_op(cib_t * cib, const char *op, const char *host,
const char *section, xmlNode * data,
xmlNode ** output_data, int call_options, const char *user_name);
int cib_file_read_and_verify(const char *filename, const char *sigfile,
xmlNode **root);
int cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname,
const char *cib_filename);
void cib__set_output(cib_t *cib, pcmk__output_t *out);
cib_callback_client_t* cib__lookup_id (int call_id);
/*!
* \internal
* \brief Connect to, query, and optionally disconnect from the CIB
*
* Open a read-write connection to the CIB manager if an already connected
* client is not passed in. Then query the CIB and store the resulting XML.
* Finally, disconnect if the CIB connection isn't being returned to the caller.
*
* \param[in,out] out Output object (may be \p NULL)
* \param[in,out] cib If not \p NULL, where to store CIB connection
* \param[out] cib_object Where to store query result
*
* \return Standard Pacemaker return code
*
* \note If \p cib is not \p NULL, the caller is responsible for freeing \p *cib
* using \p cib_delete().
* \note If \p *cib points to an existing \p cib_t object, this function will
* reuse it instead of creating a new one. If the existing client is
* already connected, the connection will be reused, even if it's
* read-only.
*/
int cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object);
-int cib__signon_attempts(cib_t *cib, enum cib_conn_type type, int attempts);
+int cib__create_signon(cib_t **cib);
int cib__clean_up_connection(cib_t **cib);
int cib__update_node_attr(pcmk__output_t *out, cib_t *cib, int call_options,
const char *section, const char *node_uuid, const char *set_type,
const char *set_name, const char *attr_id, const char *attr_name,
const char *attr_value, const char *user_name,
const char *node_type);
int cib__get_node_attrs(pcmk__output_t *out, cib_t *cib, const char *section,
const char *node_uuid, const char *set_type, const char *set_name,
const char *attr_id, const char *attr_name, const char *user_name,
xmlNode **result);
int cib__delete_node_attr(pcmk__output_t *out, cib_t *cib, int options,
const char *section, const char *node_uuid, const char *set_type,
const char *set_name, const char *attr_id, const char *attr_name,
const char *attr_value, const char *user_name);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_CIB_INTERNAL__H
diff --git a/include/crm/cib_compat.h b/include/crm/cib_compat.h
index 01f1d6ca78..dd0d6dbfc2 100644
--- a/include/crm/cib_compat.h
+++ b/include/crm/cib_compat.h
@@ -1,34 +1,42 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_CIB_COMPAT__H
# define PCMK__CRM_CIB_COMPAT__H
+#include // cib_t
+
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Deprecated CIB utilities
* \ingroup core
* \deprecated Do not include this header directly. The utilities in this
* header, and the header itself, will be removed in a future
* release.
*/
// NOTE: sbd (as of at least 1.5.2) uses this
//! \deprecated Do not use
#define T_CIB_DIFF_NOTIFY "cib_diff_notify"
+/*!
+ * \deprecated Save CIB_shadow, unset it, call cib_new(), and restore old
+ * CIB_shadow instead
+ */
+cib_t *cib_new_no_shadow(void);
+
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_CIB_COMPAT__H
diff --git a/lib/cib/cib_client.c b/lib/cib/cib_client.c
index 5cbbb141c0..220410dc2c 100644
--- a/lib/cib/cib_client.c
+++ b/lib/cib/cib_client.c
@@ -1,771 +1,767 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static GHashTable *cib_op_callback_table = NULL;
static gint
ciblib_GCompareFunc(gconstpointer a, gconstpointer b)
{
int rc = 0;
const cib_notify_client_t *a_client = a;
const cib_notify_client_t *b_client = b;
CRM_CHECK(a_client->event != NULL && b_client->event != NULL, return 0);
rc = strcmp(a_client->event, b_client->event);
if (rc == 0) {
if (a_client->callback == b_client->callback) {
return 0;
} else if (((long)a_client->callback) < ((long)b_client->callback)) {
crm_trace("callbacks for %s are not equal: %p < %p",
a_client->event, a_client->callback, b_client->callback);
return -1;
}
crm_trace("callbacks for %s are not equal: %p > %p",
a_client->event, a_client->callback, b_client->callback);
return 1;
}
return rc;
}
static int
cib_client_add_notify_callback(cib_t * cib, const char *event,
void (*callback) (const char *event,
xmlNode * msg))
{
GList *list_item = NULL;
cib_notify_client_t *new_client = NULL;
if ((cib->variant != cib_native) && (cib->variant != cib_remote)) {
return -EPROTONOSUPPORT;
}
crm_trace("Adding callback for %s events (%d)",
event, g_list_length(cib->notify_list));
new_client = pcmk__assert_alloc(1, sizeof(cib_notify_client_t));
new_client->event = event;
new_client->callback = callback;
list_item = g_list_find_custom(cib->notify_list, new_client,
ciblib_GCompareFunc);
if (list_item != NULL) {
crm_warn("Callback already present");
free(new_client);
return -EINVAL;
} else {
cib->notify_list = g_list_append(cib->notify_list, new_client);
cib->cmds->register_notification(cib, event, 1);
crm_trace("Callback added (%d)", g_list_length(cib->notify_list));
}
return pcmk_ok;
}
static int
get_notify_list_event_count(cib_t *cib, const char *event)
{
int count = 0;
for (GList *iter = g_list_first(cib->notify_list); iter != NULL;
iter = iter->next) {
cib_notify_client_t *client = (cib_notify_client_t *) iter->data;
if (strcmp(client->event, event) == 0) {
count++;
}
}
crm_trace("event(%s) count : %d", event, count);
return count;
}
static int
cib_client_del_notify_callback(cib_t *cib, const char *event,
void (*callback) (const char *event,
xmlNode *msg))
{
GList *list_item = NULL;
cib_notify_client_t *new_client = NULL;
if (cib->variant != cib_native && cib->variant != cib_remote) {
return -EPROTONOSUPPORT;
}
if (get_notify_list_event_count(cib, event) == 0) {
crm_debug("The callback of the event does not exist(%s)", event);
return pcmk_ok;
}
crm_debug("Removing callback for %s events", event);
new_client = pcmk__assert_alloc(1, sizeof(cib_notify_client_t));
new_client->event = event;
new_client->callback = callback;
list_item = g_list_find_custom(cib->notify_list, new_client, ciblib_GCompareFunc);
if (list_item != NULL) {
cib_notify_client_t *list_client = list_item->data;
cib->notify_list = g_list_remove(cib->notify_list, list_client);
free(list_client);
crm_trace("Removed callback");
} else {
crm_trace("Callback not present");
}
if (get_notify_list_event_count(cib, event) == 0) {
/* When there is not the registration of the event, the processing turns off a notice. */
cib->cmds->register_notification(cib, event, 0);
}
free(new_client);
return pcmk_ok;
}
static gboolean
cib_async_timeout_handler(gpointer data)
{
struct timer_rec_s *timer = data;
crm_debug("Async call %d timed out after %ds",
timer->call_id, timer->timeout);
cib_native_callback(timer->cib, NULL, timer->call_id, -ETIME);
// We remove the handler in remove_cib_op_callback()
return G_SOURCE_CONTINUE;
}
static gboolean
cib_client_register_callback_full(cib_t *cib, int call_id, int timeout,
gboolean only_success, void *user_data,
const char *callback_name,
void (*callback)(xmlNode *, int, int,
xmlNode *, void *),
void (*free_func)(void *))
{
cib_callback_client_t *blob = NULL;
if (call_id < 0) {
if (only_success == FALSE) {
callback(NULL, call_id, call_id, NULL, user_data);
} else {
crm_warn("CIB call failed: %s", pcmk_strerror(call_id));
}
if (user_data && free_func) {
free_func(user_data);
}
return FALSE;
}
blob = pcmk__assert_alloc(1, sizeof(cib_callback_client_t));
blob->id = callback_name;
blob->only_success = only_success;
blob->user_data = user_data;
blob->callback = callback;
blob->free_func = free_func;
if (timeout > 0) {
struct timer_rec_s *async_timer =
pcmk__assert_alloc(1, sizeof(struct timer_rec_s));
blob->timer = async_timer;
async_timer->cib = cib;
async_timer->call_id = call_id;
async_timer->timeout = timeout * 1000;
async_timer->ref = pcmk__create_timer(async_timer->timeout,
cib_async_timeout_handler,
async_timer);
}
crm_trace("Adding callback %s for call %d", callback_name, call_id);
pcmk__intkey_table_insert(cib_op_callback_table, call_id, blob);
return TRUE;
}
static gboolean
cib_client_register_callback(cib_t *cib, int call_id, int timeout,
gboolean only_success, void *user_data,
const char *callback_name,
void (*callback) (xmlNode *, int, int, xmlNode *,
void *))
{
return cib_client_register_callback_full(cib, call_id, timeout,
only_success, user_data,
callback_name, callback, NULL);
}
static int
cib_client_noop(cib_t * cib, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_NOOP, NULL, NULL, NULL, NULL,
call_options, cib->user);
}
static int
cib_client_ping(cib_t * cib, xmlNode ** output_data, int call_options)
{
return cib_internal_op(cib, CRM_OP_PING, NULL, NULL, NULL, output_data,
call_options, cib->user);
}
static int
cib_client_query(cib_t * cib, const char *section, xmlNode ** output_data, int call_options)
{
return cib->cmds->query_from(cib, NULL, section, output_data, call_options);
}
static int
cib_client_query_from(cib_t * cib, const char *host, const char *section,
xmlNode ** output_data, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_QUERY, host, section, NULL,
output_data, call_options, cib->user);
}
static int
set_secondary(cib_t *cib, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_SECONDARY, NULL, NULL, NULL,
NULL, call_options, cib->user);
}
static int
set_primary(cib_t *cib, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_PRIMARY, NULL, NULL, NULL,
NULL, call_options, cib->user);
}
static int
cib_client_bump_epoch(cib_t * cib, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_BUMP, NULL, NULL, NULL, NULL,
call_options, cib->user);
}
static int
cib_client_upgrade(cib_t * cib, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_UPGRADE, NULL, NULL, NULL,
NULL, call_options, cib->user);
}
static int
cib_client_sync(cib_t * cib, const char *section, int call_options)
{
return cib->cmds->sync_from(cib, NULL, section, call_options);
}
static int
cib_client_sync_from(cib_t * cib, const char *host, const char *section, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_SYNC_TO_ALL, host, section,
NULL, NULL, call_options, cib->user);
}
static int
cib_client_create(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_CREATE, NULL, section, data,
NULL, call_options, cib->user);
}
static int
cib_client_modify(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_MODIFY, NULL, section, data,
NULL, call_options, cib->user);
}
static int
cib_client_replace(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_REPLACE, NULL, section, data,
NULL, call_options, cib->user);
}
static int
cib_client_delete(cib_t * cib, const char *section, xmlNode * data, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_DELETE, NULL, section, data,
NULL, call_options, cib->user);
}
static int
cib_client_erase(cib_t * cib, xmlNode ** output_data, int call_options)
{
return cib_internal_op(cib, PCMK__CIB_REQUEST_ERASE, NULL, NULL, NULL,
output_data, call_options, cib->user);
}
static int
cib_client_init_transaction(cib_t *cib)
{
int rc = pcmk_rc_ok;
if (cib == NULL) {
return -EINVAL;
}
if (cib->transaction != NULL) {
// A client can have at most one transaction at a time
rc = pcmk_rc_already;
}
if (rc == pcmk_rc_ok) {
cib->transaction = pcmk__xe_create(NULL, PCMK__XE_CIB_TRANSACTION);
}
if (rc != pcmk_rc_ok) {
const char *client_id = NULL;
cib->cmds->client_id(cib, NULL, &client_id);
crm_err("Failed to initialize CIB transaction for client %s: %s",
client_id, pcmk_rc_str(rc));
}
return pcmk_rc2legacy(rc);
}
static int
cib_client_end_transaction(cib_t *cib, bool commit, int call_options)
{
const char *client_id = NULL;
int rc = pcmk_ok;
if (cib == NULL) {
return -EINVAL;
}
cib->cmds->client_id(cib, NULL, &client_id);
client_id = pcmk__s(client_id, "(unidentified)");
if (commit) {
if (cib->transaction == NULL) {
rc = pcmk_rc_no_transaction;
crm_err("Failed to commit transaction for CIB client %s: %s",
client_id, pcmk_rc_str(rc));
return pcmk_rc2legacy(rc);
}
rc = cib_internal_op(cib, PCMK__CIB_REQUEST_COMMIT_TRANSACT, NULL, NULL,
cib->transaction, NULL, call_options, cib->user);
} else {
// Discard always succeeds
if (cib->transaction != NULL) {
crm_trace("Discarded transaction for CIB client %s", client_id);
} else {
crm_trace("No transaction found for CIB client %s", client_id);
}
}
pcmk__xml_free(cib->transaction);
cib->transaction = NULL;
return rc;
}
static int
cib_client_fetch_schemas(cib_t *cib, xmlNode **output_data, const char *after_ver,
int call_options)
{
xmlNode *data = pcmk__xe_create(NULL, PCMK__XA_SCHEMA);
int rc = pcmk_ok;
crm_xml_add(data, PCMK_XA_VERSION, after_ver);
rc = cib_internal_op(cib, PCMK__CIB_REQUEST_SCHEMAS, NULL, NULL, data,
output_data, call_options, NULL);
pcmk__xml_free(data);
return rc;
}
static void
cib_client_set_user(cib_t *cib, const char *user)
{
pcmk__str_update(&(cib->user), user);
}
static void
cib_destroy_op_callback(gpointer data)
{
cib_callback_client_t *blob = data;
if (blob->timer && blob->timer->ref > 0) {
g_source_remove(blob->timer->ref);
}
free(blob->timer);
if (blob->user_data && blob->free_func) {
blob->free_func(blob->user_data);
}
free(blob);
}
static void
destroy_op_callback_table(void)
{
if (cib_op_callback_table != NULL) {
g_hash_table_destroy(cib_op_callback_table);
cib_op_callback_table = NULL;
}
}
char *
get_shadow_file(const char *suffix)
{
char *cib_home = NULL;
char *fullname = NULL;
char *name = crm_strdup_printf("shadow.%s", suffix);
const char *dir = getenv("CIB_shadow_dir");
if (dir == NULL) {
uid_t uid = geteuid();
struct passwd *pwent = getpwuid(uid);
const char *user = NULL;
if (pwent) {
user = pwent->pw_name;
} else {
user = getenv("USER");
crm_perror(LOG_ERR,
"Assuming %s because cannot get user details for user ID %d",
(user? user : "unprivileged user"), uid);
}
if (pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) {
dir = CRM_CONFIG_DIR;
} else {
const char *home = NULL;
if ((home = getenv("HOME")) == NULL) {
if (pwent) {
home = pwent->pw_dir;
}
}
dir = pcmk__get_tmpdir();
if (home && home[0] == '/') {
int rc = 0;
cib_home = crm_strdup_printf("%s/.cib", home);
rc = mkdir(cib_home, 0700);
if (rc < 0 && errno != EEXIST) {
crm_perror(LOG_ERR, "Couldn't create user-specific shadow directory: %s",
cib_home);
errno = 0;
} else {
dir = cib_home;
}
}
}
}
fullname = crm_strdup_printf("%s/%s", dir, name);
free(cib_home);
free(name);
return fullname;
}
cib_t *
cib_shadow_new(const char *shadow)
{
cib_t *new_cib = NULL;
char *shadow_file = NULL;
CRM_CHECK(shadow != NULL, return NULL);
shadow_file = get_shadow_file(shadow);
new_cib = cib_file_new(shadow_file);
free(shadow_file);
return new_cib;
}
-/*!
- * \brief Create a new CIB connection object, ignoring any active shadow CIB
- *
- * Create a new live, file, or remote CIB connection object based on the values
- * of CIB-related environment variables (CIB_file, CIB_port, CIB_server,
- * CIB_user, and CIB_passwd). The object will not be connected.
- *
- * \return Newly allocated CIB connection object
- * \note The CIB API does not fully support opening multiple CIB connection
- * objects simultaneously, so the returned object should be treated as a
- * singleton.
- */
-cib_t *
-cib_new_no_shadow(void)
-{
- const char *shadow = getenv("CIB_shadow");
- cib_t *cib = NULL;
-
- unsetenv("CIB_shadow");
- cib = cib_new();
-
- if (shadow != NULL) {
- setenv("CIB_shadow", shadow, 1);
- }
- return cib;
-}
-
/*!
* \brief Create a new CIB connection object
*
* Create a new live, remote, file, or shadow file CIB connection object based
* on the values of CIB-related environment variables (CIB_shadow, CIB_file,
* CIB_port, CIB_server, CIB_user, and CIB_passwd). The object will not be
* connected.
*
* \return Newly allocated CIB connection object
* \note The CIB API does not fully support opening multiple CIB connection
* objects simultaneously, so the returned object should be treated as a
* singleton.
*/
/* @TODO Ensure all APIs support multiple simultaneous CIB connection objects
* (at least cib_free_callbacks() currently does not).
*/
cib_t *
cib_new(void)
{
const char *value = getenv("CIB_shadow");
const char *server = NULL;
const char *user = NULL;
const char *pass = NULL;
gboolean encrypted = TRUE;
int port;
if (!pcmk__str_empty(value)) {
return cib_shadow_new(value);
}
value = getenv("CIB_file");
if (!pcmk__str_empty(value)) {
return cib_file_new(value);
}
value = getenv("CIB_port");
if (pcmk__str_empty(value)) {
return cib_native_new();
}
/* We don't ensure port is valid (>= 0) because cib_new() currently can't
* return NULL in practice, and introducing a NULL return here could cause
* core dumps that would previously just cause signon() failures.
*/
pcmk__scan_port(value, &port);
if (!crm_is_true(getenv("CIB_encrypted"))) {
encrypted = FALSE;
}
server = getenv("CIB_server");
user = getenv("CIB_user");
pass = getenv("CIB_passwd");
if (pcmk__str_empty(user)) {
user = CRM_DAEMON_USER;
}
if (pcmk__str_empty(server)) {
server = "localhost";
}
crm_debug("Initializing %s remote CIB access to %s:%d as user %s",
(encrypted? "encrypted" : "plain-text"), server, port, user);
return cib_remote_new(server, user, pass, port, encrypted);
}
/*!
* \internal
* \brief Create a generic CIB connection instance
*
* \return Newly allocated and initialized cib_t instance
*
* \note This is called by each variant's cib_*_new() function before setting
* variant-specific values.
*/
cib_t *
cib_new_variant(void)
{
cib_t *new_cib = NULL;
new_cib = calloc(1, sizeof(cib_t));
if (new_cib == NULL) {
return NULL;
}
remove_cib_op_callback(0, TRUE); /* remove all */
new_cib->call_id = 1;
new_cib->variant = cib_undefined;
new_cib->type = cib_no_connection;
new_cib->state = cib_disconnected;
new_cib->variant_opaque = NULL;
new_cib->notify_list = NULL;
/* the rest will get filled in by the variant constructor */
new_cib->cmds = calloc(1, sizeof(cib_api_operations_t));
if (new_cib->cmds == NULL) {
free(new_cib);
return NULL;
}
new_cib->cmds->add_notify_callback = cib_client_add_notify_callback;
new_cib->cmds->del_notify_callback = cib_client_del_notify_callback;
new_cib->cmds->register_callback = cib_client_register_callback;
new_cib->cmds->register_callback_full = cib_client_register_callback_full;
new_cib->cmds->noop = cib_client_noop; // Deprecated method
new_cib->cmds->ping = cib_client_ping;
new_cib->cmds->query = cib_client_query;
new_cib->cmds->sync = cib_client_sync;
new_cib->cmds->query_from = cib_client_query_from;
new_cib->cmds->sync_from = cib_client_sync_from;
new_cib->cmds->set_primary = set_primary;
new_cib->cmds->set_secondary = set_secondary;
new_cib->cmds->upgrade = cib_client_upgrade;
new_cib->cmds->bump_epoch = cib_client_bump_epoch;
new_cib->cmds->create = cib_client_create;
new_cib->cmds->modify = cib_client_modify;
new_cib->cmds->replace = cib_client_replace;
new_cib->cmds->remove = cib_client_delete;
new_cib->cmds->erase = cib_client_erase;
new_cib->cmds->init_transaction = cib_client_init_transaction;
new_cib->cmds->end_transaction = cib_client_end_transaction;
new_cib->cmds->set_user = cib_client_set_user;
new_cib->cmds->fetch_schemas = cib_client_fetch_schemas;
return new_cib;
}
void
cib_free_notify(cib_t *cib)
{
if (cib) {
GList *list = cib->notify_list;
while (list != NULL) {
cib_notify_client_t *client = g_list_nth_data(list, 0);
list = g_list_remove(list, client);
free(client);
}
cib->notify_list = NULL;
}
}
/*!
* \brief Free all callbacks for a CIB connection
*
* \param[in,out] cib CIB connection to clean up
*/
void
cib_free_callbacks(cib_t *cib)
{
cib_free_notify(cib);
destroy_op_callback_table();
}
/*!
* \brief Free all memory used by CIB connection
*
* \param[in,out] cib CIB connection to delete
*/
void
cib_delete(cib_t *cib)
{
cib_free_callbacks(cib);
if (cib) {
cib->cmds->free(cib);
}
}
void
remove_cib_op_callback(int call_id, gboolean all_callbacks)
{
if (all_callbacks) {
destroy_op_callback_table();
cib_op_callback_table = pcmk__intkey_table(cib_destroy_op_callback);
} else {
pcmk__intkey_table_remove(cib_op_callback_table, call_id);
}
}
int
num_cib_op_callbacks(void)
{
if (cib_op_callback_table == NULL) {
return 0;
}
return g_hash_table_size(cib_op_callback_table);
}
static void
cib_dump_pending_op(gpointer key, gpointer value, gpointer user_data)
{
int call = GPOINTER_TO_INT(key);
cib_callback_client_t *blob = value;
crm_debug("Call %d (%s): pending", call, pcmk__s(blob->id, "without ID"));
}
void
cib_dump_pending_callbacks(void)
{
if (cib_op_callback_table == NULL) {
return;
}
return g_hash_table_foreach(cib_op_callback_table, cib_dump_pending_op, NULL);
}
cib_callback_client_t*
cib__lookup_id (int call_id)
{
return pcmk__intkey_table_lookup(cib_op_callback_table, call_id);
}
+
+// Deprecated functions kept only for backward API compatibility
+// LCOV_EXCL_START
+
+#include
+
+cib_t *
+cib_new_no_shadow(void)
+{
+ const char *shadow = getenv("CIB_shadow");
+ cib_t *cib = NULL;
+
+ unsetenv("CIB_shadow");
+ cib = cib_new();
+
+ if (shadow != NULL) {
+ setenv("CIB_shadow", shadow, 1);
+ }
+ return cib;
+}
+
+// LCOV_EXCL_STOP
+// End deprecated API
diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c
index e591550d0f..ae462de74d 100644
--- a/lib/cib/cib_utils.c
+++ b/lib/cib/cib_utils.c
@@ -1,956 +1,983 @@
/*
* Original copyright 2004 International Business Machines
* Later changes copyright 2008-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
gboolean
cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates)
{
*epoch = -1;
*updates = -1;
*admin_epoch = -1;
if (cib == NULL) {
return FALSE;
} else {
crm_element_value_int(cib, PCMK_XA_EPOCH, epoch);
crm_element_value_int(cib, PCMK_XA_NUM_UPDATES, updates);
crm_element_value_int(cib, PCMK_XA_ADMIN_EPOCH, admin_epoch);
}
return TRUE;
}
gboolean
cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates,
int *_admin_epoch, int *_epoch, int *_updates)
{
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
pcmk__xml_patchset_versions(diff, del, add);
*admin_epoch = add[0];
*epoch = add[1];
*updates = add[2];
*_admin_epoch = del[0];
*_epoch = del[1];
*_updates = del[2];
return TRUE;
}
/*!
* \internal
* \brief Get the XML patchset from a CIB diff notification
*
* \param[in] msg CIB diff notification
* \param[out] patchset Where to store XML patchset
*
* \return Standard Pacemaker return code
*/
int
cib__get_notify_patchset(const xmlNode *msg, const xmlNode **patchset)
{
int rc = pcmk_err_generic;
xmlNode *wrapper = NULL;
pcmk__assert(patchset != NULL);
*patchset = NULL;
if (msg == NULL) {
crm_err("CIB diff notification received with no XML");
return ENOMSG;
}
if ((crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc) != 0)
|| (rc != pcmk_ok)) {
crm_warn("Ignore failed CIB update: %s " QB_XS " rc=%d",
pcmk_strerror(rc), rc);
crm_log_xml_debug(msg, "failed");
return pcmk_legacy2rc(rc);
}
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL);
*patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (*patchset == NULL) {
crm_err("CIB diff notification received with no patchset");
return ENOMSG;
}
return pcmk_rc_ok;
}
/*!
* \brief Create XML for a new (empty) CIB
*
* \param[in] cib_epoch What to use as \c PCMK_XA_EPOCH CIB attribute
*
* \return Newly created XML for empty CIB
*
* \note It is the caller's responsibility to free the result with
* \c pcmk__xml_free().
*/
xmlNode *
createEmptyCib(int cib_epoch)
{
xmlNode *cib_root = NULL, *config = NULL;
cib_root = pcmk__xe_create(NULL, PCMK_XE_CIB);
crm_xml_add(cib_root, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
crm_xml_add(cib_root, PCMK_XA_VALIDATE_WITH, pcmk__highest_schema_name());
crm_xml_add_int(cib_root, PCMK_XA_EPOCH, cib_epoch);
crm_xml_add_int(cib_root, PCMK_XA_NUM_UPDATES, 0);
crm_xml_add_int(cib_root, PCMK_XA_ADMIN_EPOCH, 0);
config = pcmk__xe_create(cib_root, PCMK_XE_CONFIGURATION);
pcmk__xe_create(cib_root, PCMK_XE_STATUS);
pcmk__xe_create(config, PCMK_XE_CRM_CONFIG);
pcmk__xe_create(config, PCMK_XE_NODES);
pcmk__xe_create(config, PCMK_XE_RESOURCES);
pcmk__xe_create(config, PCMK_XE_CONSTRAINTS);
#if PCMK__RESOURCE_STICKINESS_DEFAULT != 0
{
xmlNode *rsc_defaults = pcmk__xe_create(config, PCMK_XE_RSC_DEFAULTS);
xmlNode *meta = pcmk__xe_create(rsc_defaults, PCMK_XE_META_ATTRIBUTES);
xmlNode *nvpair = pcmk__xe_create(meta, PCMK_XE_NVPAIR);
crm_xml_add(meta, PCMK_XA_ID, "build-resource-defaults");
crm_xml_add(nvpair, PCMK_XA_ID, "build-" PCMK_META_RESOURCE_STICKINESS);
crm_xml_add(nvpair, PCMK_XA_NAME, PCMK_META_RESOURCE_STICKINESS);
crm_xml_add_int(nvpair, PCMK_XA_VALUE,
PCMK__RESOURCE_STICKINESS_DEFAULT);
}
#endif
return cib_root;
}
static bool
cib_acl_enabled(xmlNode *xml, const char *user)
{
bool rc = FALSE;
if(pcmk_acl_required(user)) {
const char *value = NULL;
GHashTable *options = pcmk__strkey_table(free, free);
cib_read_config(options, xml);
value = pcmk__cluster_option(options, PCMK_OPT_ENABLE_ACL);
rc = crm_is_true(value);
g_hash_table_destroy(options);
}
crm_trace("CIB ACL is %s", rc ? "enabled" : "disabled");
return rc;
}
/*!
* \internal
* \brief Determine whether to perform operations on a scratch copy of the CIB
*
* \param[in] op CIB operation
* \param[in] section CIB section
* \param[in] call_options CIB call options
*
* \return \p true if we should make a copy of the CIB, or \p false otherwise
*/
static bool
should_copy_cib(const char *op, const char *section, int call_options)
{
if (pcmk_is_set(call_options, cib_dryrun)) {
// cib_dryrun implies a scratch copy by definition; no side effects
return true;
}
if (pcmk__str_eq(op, PCMK__CIB_REQUEST_COMMIT_TRANSACT, pcmk__str_none)) {
/* Commit-transaction must make a copy for atomicity. We must revert to
* the original CIB if the entire transaction cannot be applied
* successfully.
*/
return true;
}
if (pcmk_is_set(call_options, cib_transaction)) {
/* If cib_transaction is set, then we're in the process of committing a
* transaction. The commit-transaction request already made a scratch
* copy, and we're accumulating changes in that copy.
*/
return false;
}
if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_none)) {
/* Copying large CIBs accounts for a huge percentage of our CIB usage,
* and this avoids some of it.
*
* @TODO: Is this safe? See discussion at
* https://github.com/ClusterLabs/pacemaker/pull/3094#discussion_r1211400690.
*/
return false;
}
// Default behavior is to operate on a scratch copy
return true;
}
int
cib_perform_op(cib_t *cib, const char *op, uint32_t call_options,
cib__op_fn_t fn, bool is_query, const char *section,
xmlNode *req, xmlNode *input, bool manage_counters,
bool *config_changed, xmlNode **current_cib,
xmlNode **result_cib, xmlNode **diff, xmlNode **output)
{
int rc = pcmk_ok;
bool check_schema = true;
bool make_copy = true;
xmlNode *top = NULL;
xmlNode *scratch = NULL;
xmlNode *patchset_cib = NULL;
xmlNode *local_diff = NULL;
const char *user = crm_element_value(req, PCMK__XA_CIB_USER);
const bool enable_acl = cib_acl_enabled(*current_cib, user);
bool with_digest = false;
crm_trace("Begin %s%s%s op",
(pcmk_is_set(call_options, cib_dryrun)? "dry run of " : ""),
(is_query? "read-only " : ""), op);
CRM_CHECK(output != NULL, return -ENOMSG);
CRM_CHECK(current_cib != NULL, return -ENOMSG);
CRM_CHECK(result_cib != NULL, return -ENOMSG);
CRM_CHECK(config_changed != NULL, return -ENOMSG);
if(output) {
*output = NULL;
}
*result_cib = NULL;
*config_changed = false;
if (fn == NULL) {
return -EINVAL;
}
if (is_query) {
xmlNode *cib_ro = *current_cib;
xmlNode *cib_filtered = NULL;
if (enable_acl
&& xml_acl_filtered_copy(user, *current_cib, *current_cib,
&cib_filtered)) {
if (cib_filtered == NULL) {
crm_debug("Pre-filtered the entire cib");
return -EACCES;
}
cib_ro = cib_filtered;
crm_log_xml_trace(cib_ro, "filtered");
}
rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output);
if(output == NULL || *output == NULL) {
/* nothing */
} else if(cib_filtered == *output) {
cib_filtered = NULL; /* Let them have this copy */
} else if (*output == *current_cib) {
/* They already know not to free it */
} else if(cib_filtered && (*output)->doc == cib_filtered->doc) {
/* We're about to free the document of which *output is a part */
*output = pcmk__xml_copy(NULL, *output);
} else if ((*output)->doc == (*current_cib)->doc) {
/* Give them a copy they can free */
*output = pcmk__xml_copy(NULL, *output);
}
pcmk__xml_free(cib_filtered);
return rc;
}
make_copy = should_copy_cib(op, section, call_options);
if (!make_copy) {
/* Conditional on v2 patch style */
scratch = *current_cib;
// Make a copy of the top-level element to store version details
top = pcmk__xe_create(NULL, (const char *) scratch->name);
pcmk__xe_copy_attrs(top, scratch, pcmk__xaf_none);
patchset_cib = top;
pcmk__xml_commit_changes(scratch->doc);
pcmk__xml_doc_set_flags(scratch->doc, pcmk__xf_tracking);
if (enable_acl) {
pcmk__enable_acl(*current_cib, scratch, user);
}
rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output);
/* If scratch points to a new object now (for example, after an erase
* operation), then *current_cib should point to the same object.
*
* @TODO Enable tracking and ACLs and calculate changes? Change tracking
* and unpacked ACLs didn't carry over to new object.
*/
*current_cib = scratch;
} else {
scratch = pcmk__xml_copy(NULL, *current_cib);
patchset_cib = *current_cib;
pcmk__xml_doc_set_flags(scratch->doc, pcmk__xf_tracking);
if (enable_acl) {
pcmk__enable_acl(*current_cib, scratch, user);
}
rc = (*fn) (op, call_options, section, req, input, *current_cib,
&scratch, output);
/* @TODO This appears to be a hack to determine whether scratch points
* to a new object now, without saving the old pointer (which may be
* invalid now) for comparison. Confirm this, and check more clearly.
*/
if (!pcmk__xml_doc_all_flags_set(scratch->doc, pcmk__xf_tracking)) {
crm_trace("Inferring changes after %s op", op);
pcmk__xml_commit_changes(scratch->doc);
if (enable_acl) {
pcmk__enable_acl(*current_cib, scratch, user);
}
pcmk__xml_mark_changes(*current_cib, scratch);
}
CRM_CHECK(*current_cib != scratch, return -EINVAL);
}
xml_acl_disable(scratch); /* Allow the system to make any additional changes */
if (rc == pcmk_ok && scratch == NULL) {
rc = -EINVAL;
goto done;
} else if(rc == pcmk_ok && xml_acl_denied(scratch)) {
crm_trace("ACL rejected part or all of the proposed changes");
rc = -EACCES;
goto done;
} else if (rc != pcmk_ok) {
goto done;
}
/* If the CIB is from a file, we don't need to check that the feature set is
* supported. All we care about in that case is the schema version, which
* is checked elsewhere.
*/
if (scratch && (cib == NULL || cib->variant != cib_file)) {
const char *new_version = crm_element_value(scratch, PCMK_XA_CRM_FEATURE_SET);
rc = pcmk__check_feature_set(new_version);
if (rc != pcmk_rc_ok) {
crm_err("Discarding update with feature set '%s' greater than "
"our own '%s'", new_version, CRM_FEATURE_SET);
rc = pcmk_rc2legacy(rc);
goto done;
}
}
if (patchset_cib != NULL) {
int old = 0;
int new = 0;
crm_element_value_int(scratch, PCMK_XA_ADMIN_EPOCH, &new);
crm_element_value_int(patchset_cib, PCMK_XA_ADMIN_EPOCH, &old);
if (old > new) {
crm_err("%s went backwards: %d -> %d (Opts: %#x)",
PCMK_XA_ADMIN_EPOCH, old, new, call_options);
crm_log_xml_warn(req, "Bad Op");
crm_log_xml_warn(input, "Bad Data");
rc = -pcmk_err_old_data;
} else if (old == new) {
crm_element_value_int(scratch, PCMK_XA_EPOCH, &new);
crm_element_value_int(patchset_cib, PCMK_XA_EPOCH, &old);
if (old > new) {
crm_err("%s went backwards: %d -> %d (Opts: %#x)",
PCMK_XA_EPOCH, old, new, call_options);
crm_log_xml_warn(req, "Bad Op");
crm_log_xml_warn(input, "Bad Data");
rc = -pcmk_err_old_data;
}
}
}
crm_trace("Massaging CIB contents");
pcmk__strip_xml_text(scratch);
if (make_copy) {
static time_t expires = 0;
time_t tm_now = time(NULL);
if (expires < tm_now) {
expires = tm_now + 60; /* Validate clients are correctly applying v2-style diffs at most once a minute */
with_digest = true;
}
}
local_diff = xml_create_patchset(0, patchset_cib, scratch,
config_changed, manage_counters);
pcmk__log_xml_changes(LOG_TRACE, scratch);
pcmk__xml_commit_changes(scratch->doc);
if(local_diff) {
if (with_digest) {
pcmk__xml_patchset_add_digest(local_diff, scratch);
}
pcmk__log_xml_patchset(LOG_INFO, local_diff);
crm_log_xml_trace(local_diff, "raw patch");
}
if (make_copy && (local_diff != NULL)) {
// Original to compare against doesn't exist
pcmk__if_tracing(
{
// Validate the calculated patch set
int test_rc = pcmk_ok;
int format = 1;
xmlNode *cib_copy = pcmk__xml_copy(NULL, patchset_cib);
crm_element_value_int(local_diff, PCMK_XA_FORMAT, &format);
test_rc = xml_apply_patchset(cib_copy, local_diff,
manage_counters);
if (test_rc != pcmk_ok) {
save_xml_to_file(cib_copy, "PatchApply:calculated", NULL);
save_xml_to_file(patchset_cib, "PatchApply:input", NULL);
save_xml_to_file(scratch, "PatchApply:actual", NULL);
save_xml_to_file(local_diff, "PatchApply:diff", NULL);
crm_err("v%d patchset error, patch failed to apply: %s "
"(%d)",
format, pcmk_rc_str(pcmk_legacy2rc(test_rc)),
test_rc);
}
pcmk__xml_free(cib_copy);
},
{}
);
}
if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) {
/* Throttle the amount of costly validation we perform due to status updates
* a) we don't really care whats in the status section
* b) we don't validate any of its contents at the moment anyway
*/
check_schema = false;
}
/* === scratch must not be modified after this point ===
* Exceptions, anything in:
static filter_t filter[] = {
{ 0, PCMK_XA_CRM_DEBUG_ORIGIN },
{ 0, PCMK_XA_CIB_LAST_WRITTEN },
{ 0, PCMK_XA_UPDATE_ORIGIN },
{ 0, PCMK_XA_UPDATE_CLIENT },
{ 0, PCMK_XA_UPDATE_USER },
};
*/
if (*config_changed && !pcmk_is_set(call_options, cib_no_mtime)) {
const char *schema = crm_element_value(scratch, PCMK_XA_VALIDATE_WITH);
if (schema == NULL) {
rc = -pcmk_err_cib_corrupt;
}
pcmk__xe_add_last_written(scratch);
pcmk__warn_if_schema_deprecated(schema);
/* Make values of origin, client, and user in scratch match
* the ones in req (if the schema allows the attributes)
*/
if (pcmk__cmp_schemas_by_name(schema, "pacemaker-1.2") >= 0) {
const char *origin = crm_element_value(req, PCMK__XA_SRC);
const char *client = crm_element_value(req,
PCMK__XA_CIB_CLIENTNAME);
if (origin != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_ORIGIN, origin);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_ORIGIN);
}
if (client != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_CLIENT, user);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_CLIENT);
}
if (user != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_USER, user);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_USER);
}
}
}
crm_trace("Perform validation: %s", pcmk__btoa(check_schema));
if ((rc == pcmk_ok) && check_schema
&& !pcmk__configured_schema_validates(scratch)) {
rc = -pcmk_err_schema_validation;
}
done:
*result_cib = scratch;
/* @TODO: This may not work correctly with !make_copy, since we don't
* keep the original CIB.
*/
if ((rc != pcmk_ok) && cib_acl_enabled(patchset_cib, user)
&& xml_acl_filtered_copy(user, patchset_cib, scratch, result_cib)) {
if (*result_cib == NULL) {
crm_debug("Pre-filtered the entire cib result");
}
pcmk__xml_free(scratch);
}
if(diff) {
*diff = local_diff;
} else {
pcmk__xml_free(local_diff);
}
pcmk__xml_free(top);
crm_trace("Done");
return rc;
}
int
cib__create_op(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data, int call_options,
const char *user_name, const char *client_name,
xmlNode **op_msg)
{
CRM_CHECK((cib != NULL) && (op_msg != NULL), return -EPROTO);
*op_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND);
cib->call_id++;
if (cib->call_id < 1) {
cib->call_id = 1;
}
crm_xml_add(*op_msg, PCMK__XA_T, PCMK__VALUE_CIB);
crm_xml_add(*op_msg, PCMK__XA_CIB_OP, op);
crm_xml_add(*op_msg, PCMK__XA_CIB_HOST, host);
crm_xml_add(*op_msg, PCMK__XA_CIB_SECTION, section);
crm_xml_add(*op_msg, PCMK__XA_CIB_USER, user_name);
crm_xml_add(*op_msg, PCMK__XA_CIB_CLIENTNAME, client_name);
crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLID, cib->call_id);
crm_trace("Sending call options: %.8lx, %d", (long)call_options, call_options);
crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLOPT, call_options);
if (data != NULL) {
xmlNode *wrapper = pcmk__xe_create(*op_msg, PCMK__XE_CIB_CALLDATA);
pcmk__xml_copy(wrapper, data);
}
return pcmk_ok;
}
/*!
* \internal
* \brief Check whether a CIB request is supported in a transaction
*
* \param[in] request CIB request
*
* \return Standard Pacemaker return code
*/
static int
validate_transaction_request(const xmlNode *request)
{
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *host = crm_element_value(request, PCMK__XA_CIB_HOST);
const cib__operation_t *operation = NULL;
int rc = cib__get_operation(op, &operation);
if (rc != pcmk_rc_ok) {
// cib__get_operation() logs error
return rc;
}
if (!pcmk_is_set(operation->flags, cib__op_attr_transaction)) {
crm_err("Operation %s is not supported in CIB transactions", op);
return EOPNOTSUPP;
}
if (host != NULL) {
crm_err("Operation targeting a specific node (%s) is not supported in "
"a CIB transaction",
host);
return EOPNOTSUPP;
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Append a CIB request to a CIB transaction
*
* \param[in,out] cib CIB client whose transaction to extend
* \param[in,out] request Request to add to transaction
*
* \return Legacy Pacemaker return code
*/
int
cib__extend_transaction(cib_t *cib, xmlNode *request)
{
int rc = pcmk_rc_ok;
pcmk__assert((cib != NULL) && (request != NULL));
rc = validate_transaction_request(request);
if ((rc == pcmk_rc_ok) && (cib->transaction == NULL)) {
rc = pcmk_rc_no_transaction;
}
if (rc == pcmk_rc_ok) {
pcmk__xml_copy(cib->transaction, request);
} else {
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *client_id = NULL;
cib->cmds->client_id(cib, NULL, &client_id);
crm_err("Failed to add '%s' operation to transaction for client %s: %s",
op, pcmk__s(client_id, "(unidentified)"), pcmk_rc_str(rc));
crm_log_xml_info(request, "failed");
}
return pcmk_rc2legacy(rc);
}
void
cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc)
{
xmlNode *output = NULL;
cib_callback_client_t *blob = NULL;
if (msg != NULL) {
xmlNode *wrapper = NULL;
crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc);
crm_element_value_int(msg, PCMK__XA_CIB_CALLID, &call_id);
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_CALLDATA, NULL, NULL);
output = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
}
blob = cib__lookup_id(call_id);
if (blob == NULL) {
crm_trace("No callback found for call %d", call_id);
}
if (cib == NULL) {
crm_debug("No cib object supplied");
}
if (rc == -pcmk_err_diff_resync) {
/* This is an internal value that clients do not and should not care about */
rc = pcmk_ok;
}
if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) {
crm_trace("Invoking callback %s for call %d",
pcmk__s(blob->id, "without ID"), call_id);
blob->callback(msg, call_id, rc, output, blob->user_data);
} else if ((cib != NULL) && (rc != pcmk_ok)) {
crm_warn("CIB command failed: %s", pcmk_strerror(rc));
crm_log_xml_debug(msg, "Failed CIB Update");
}
/* This may free user_data, so do it after the callback */
if (blob) {
remove_cib_op_callback(call_id, FALSE);
}
crm_trace("OP callback activated for %d", call_id);
}
void
cib_native_notify(gpointer data, gpointer user_data)
{
xmlNode *msg = user_data;
cib_notify_client_t *entry = data;
const char *event = NULL;
if (msg == NULL) {
crm_warn("Skipping callback - NULL message");
return;
}
event = crm_element_value(msg, PCMK__XA_SUBT);
if (entry == NULL) {
crm_warn("Skipping callback - NULL callback client");
return;
} else if (entry->callback == NULL) {
crm_warn("Skipping callback - NULL callback");
return;
} else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) {
crm_trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event);
return;
}
crm_trace("Invoking callback for %p/%s event...", entry, event);
entry->callback(event, msg);
crm_trace("Callback invoked...");
}
gboolean
cib_read_config(GHashTable * options, xmlNode * current_cib)
{
xmlNode *config = NULL;
crm_time_t *now = NULL;
if (options == NULL || current_cib == NULL) {
return FALSE;
}
now = crm_time_new(NULL);
g_hash_table_remove_all(options);
config = pcmk_find_cib_element(current_cib, PCMK_XE_CRM_CONFIG);
if (config) {
pcmk_rule_input_t rule_input = {
.now = now,
};
pcmk_unpack_nvpair_blocks(config, PCMK_XE_CLUSTER_PROPERTY_SET,
PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS, &rule_input,
options, NULL);
}
pcmk__validate_cluster_options(options);
crm_time_free(now);
return TRUE;
}
int
cib_internal_op(cib_t * cib, const char *op, const char *host,
const char *section, xmlNode * data,
xmlNode ** output_data, int call_options, const char *user_name)
{
int (*delegate)(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data, xmlNode **output_data,
int call_options, const char *user_name) = NULL;
if (cib == NULL) {
return -EINVAL;
}
delegate = cib->delegate_fn;
if (delegate == NULL) {
return -EPROTONOSUPPORT;
}
if (user_name == NULL) {
user_name = getenv("CIB_user");
}
return delegate(cib, op, host, section, data, output_data, call_options, user_name);
}
/*!
* \brief Apply a CIB update patch to a given CIB
*
* \param[in] event CIB update patch
* \param[in] input CIB to patch
* \param[out] output Resulting CIB after patch
* \param[in] level Log the patch at this log level (unless LOG_CRIT)
*
* \return Legacy Pacemaker return code
* \note sbd calls this function
*/
int
cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output,
int level)
{
int rc = pcmk_err_generic;
xmlNode *wrapper = NULL;
xmlNode *diff = NULL;
pcmk__assert((event != NULL) && (input != NULL) && (output != NULL));
crm_element_value_int(event, PCMK__XA_CIB_RC, &rc);
wrapper = pcmk__xe_first_child(event, PCMK__XE_CIB_UPDATE_RESULT, NULL,
NULL);
diff = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (rc < pcmk_ok || diff == NULL) {
return rc;
}
if (level > LOG_CRIT) {
pcmk__log_xml_patchset(level, diff);
}
if (input != NULL) {
rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output,
NULL);
if (rc != pcmk_ok) {
crm_debug("Update didn't apply: %s (%d) %p",
pcmk_strerror(rc), rc, *output);
if (rc == -pcmk_err_old_data) {
crm_trace("Masking error, we already have the supplied update");
return pcmk_ok;
}
pcmk__xml_free(*output);
*output = NULL;
return rc;
}
}
return rc;
}
#define log_signon_query_err(out, fmt, args...) do { \
if (out != NULL) { \
out->err(out, fmt, ##args); \
} else { \
crm_err(fmt, ##args); \
} \
} while (0)
int
cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object)
{
int rc = pcmk_rc_ok;
cib_t *cib_conn = NULL;
pcmk__assert(cib_object != NULL);
if (cib == NULL) {
cib_conn = cib_new();
} else {
if (*cib == NULL) {
*cib = cib_new();
}
cib_conn = *cib;
}
if (cib_conn == NULL) {
return ENOMEM;
}
if (cib_conn->state == cib_disconnected) {
rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
rc = pcmk_legacy2rc(rc);
}
if (rc != pcmk_rc_ok) {
log_signon_query_err(out, "Could not connect to the CIB: %s",
pcmk_rc_str(rc));
goto done;
}
if (out != NULL) {
out->transient(out, "Querying CIB...");
}
rc = cib_conn->cmds->query(cib_conn, NULL, cib_object, cib_sync_call);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc));
}
done:
if (cib == NULL) {
cib__clean_up_connection(&cib_conn);
}
if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) {
return pcmk_rc_no_input;
}
return rc;
}
+/*!
+ * \internal
+ * \brief Create a new CIB connection object and connect to the CIB API
+ *
+ * This function attempts to connect up to 5 times.
+ *
+ * \param[out] cib Where to store CIB connection object
+ *
+ * \return Standard Pacemaker return code
+ *
+ * \note The caller is responsible for signing off and freeing the newly
+ * allocated CIB connection object using the \c signoff() method and
+ * \c cib_delete().
+ */
int
-cib__signon_attempts(cib_t *cib, enum cib_conn_type type, int attempts)
+cib__create_signon(cib_t **cib)
{
+ static const int attempts = 5;
int rc = pcmk_rc_ok;
- crm_trace("Attempting connection to CIB manager (up to %d time%s)",
+ pcmk__assert((cib != NULL) && (*cib == NULL));
+
+ *cib = cib_new();
+ if (*cib == NULL) {
+ return ENOMEM;
+ }
+
+ crm_trace("Attempting connection to CIB API (up to %d time%s)",
attempts, pcmk__plural_s(attempts));
for (int remaining = attempts - 1; remaining >= 0; --remaining) {
- rc = cib->cmds->signon(cib, crm_system_name, type);
+ rc = (*cib)->cmds->signon(*cib, crm_system_name, cib_command);
- if ((rc == pcmk_rc_ok)
+ if ((rc == pcmk_ok)
|| (remaining == 0)
|| ((errno != EAGAIN) && (errno != EALREADY))) {
break;
}
// Retry after soft error (interrupted by signal, etc.)
pcmk__sleep_ms((attempts - remaining) * 500);
crm_debug("Re-attempting connection to CIB manager (%d attempt%s remaining)",
remaining, pcmk__plural_s(remaining));
}
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ cib__clean_up_connection(cib);
+ }
+
return rc;
}
int
cib__clean_up_connection(cib_t **cib)
{
int rc;
if (*cib == NULL) {
return pcmk_rc_ok;
}
rc = (*cib)->cmds->signoff(*cib);
cib_delete(*cib);
*cib = NULL;
return pcmk_legacy2rc(rc);
}
diff --git a/tools/cibadmin.c b/tools/cibadmin.c
index bb70b0160e..464b66a963 100644
--- a/tools/cibadmin.c
+++ b/tools/cibadmin.c
@@ -1,902 +1,858 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define SUMMARY "query and edit the Pacemaker configuration"
#define INDENT " "
enum cibadmin_section_type {
cibadmin_section_all = 0,
cibadmin_section_scope,
cibadmin_section_xpath,
};
-static cib_t *the_cib = NULL;
+static cib_t *cib_conn = NULL;
static crm_exit_t exit_code = CRM_EX_OK;
static struct {
const char *cib_action;
int cmd_options;
enum cibadmin_section_type section_type;
char *cib_section;
char *validate_with;
gint message_timeout_sec;
enum pcmk__acl_render_how acl_render_mode;
gchar *cib_user;
gchar *dest_node;
gchar *input_file;
gchar *input_string;
gboolean input_stdin;
bool delete_all;
gboolean allow_create;
gboolean force;
gboolean get_node_path;
gboolean no_children;
gboolean score_update;
// @COMPAT Deprecated since 3.0.0
gboolean local;
// @COMPAT Deprecated since 3.0.1
gboolean sync_call;
} options = {
.cib_action = PCMK__CIB_REQUEST_QUERY,
.cmd_options = cib_sync_call,
+ .message_timeout_sec = 30,
};
-int do_init(void);
-static int do_work(xmlNode *input, xmlNode **output);
-
/*!
* \internal
* \brief Read input XML as specified on the command line
*
* Precedence is as follows:
* 1. Input file
* 2. Input string
* 3. stdin
*
* If multiple input sources are given, only the last occurrence of the one with
* the highest precedence is tried.
*
* If no input source is specified, this function does nothing.
*
* \param[out] input Where to store parsed input
* \param[out] error Where to store error information
*
* \return Standard Pacemaker return code
*/
static int
read_input(xmlNode **input, GError **error)
{
const char *source = NULL;
if (options.input_file != NULL) {
source = options.input_file;
*input = pcmk__xml_read(options.input_file);
} else if (options.input_string != NULL) {
source = "input string";
*input = pcmk__xml_parse(options.input_string);
} else if (options.input_stdin) {
source = "stdin";
*input = pcmk__xml_read(NULL);
} else {
*input = NULL;
return pcmk_rc_ok;
}
if (*input == NULL) {
int rc = pcmk_rc_bad_input;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Couldn't parse input from %s", source);
return rc;
}
return pcmk_rc_ok;
}
+/*!
+ * \internal
+ * \brief Output the digest of an XML tree
+ *
+ * \param[in] xml XML whose digest to output
+ * \param[in] on_disk If \c true, output the on-disk digest of \p xml
+ * \param[out] error Where to store error
+ */
static void
-print_xml_output(xmlNode * xml)
+output_digest(const xmlNode *xml, bool on_disk, GError **error)
{
- if (!xml) {
- return;
- } else if (xml->type != XML_ELEMENT_NODE) {
+ char *digest = NULL;
+
+ if (xml == NULL) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(error, PCMK__EXITC_ERROR, exit_code,
+ "Please supply XML to process with -X, -x, or -p");
return;
}
- if (pcmk_is_set(options.cmd_options, cib_xpath_address)) {
- const char *id = crm_element_value(xml, PCMK_XA_ID);
-
- if (pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) {
- xmlNode *child = NULL;
-
- for (child = xml->children; child; child = child->next) {
- print_xml_output(child);
- }
-
- } else if (id) {
- printf("%s\n", id);
- }
-
+ if (on_disk) {
+ digest = pcmk__digest_on_disk_cib(xml);
} else {
- GString *buf = g_string_sized_new(1024);
-
- pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buf, 0);
-
- fprintf(stdout, "%s", buf->str);
- g_string_free(buf, TRUE);
+ digest = pcmk__digest_xml(xml, true);
}
-}
-
-// Upgrade requested but already at latest schema
-static void
-report_schema_unchanged(void)
-{
- const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged);
-
- crm_info("Upgrade unnecessary: %s\n", err);
- printf("Upgrade unnecessary: %s\n", err);
- exit_code = CRM_EX_OK;
+ printf("%s\n", pcmk__s(digest, ""));
+ free(digest);
}
/*!
* \internal
* \brief Check whether the current CIB action is dangerous
* \return true if \p options.cib_action is dangerous, or false otherwise
*/
static inline bool
cib_action_is_dangerous(void)
{
/* @TODO Ideally, --upgrade wouldn't be considered dangerous if the CIB
* already uses the latest schema.
*/
return options.delete_all
|| pcmk__str_any_of(options.cib_action,
PCMK__CIB_REQUEST_UPGRADE,
PCMK__CIB_REQUEST_ERASE,
NULL);
}
/*!
* \internal
* \brief Determine whether the given CIB scope is valid for \p cibadmin
*
* \param[in] scope Scope to validate
*
* \return true if \p scope is valid, or false otherwise
* \note An invalid scope applies the operation to the entire CIB.
*/
static inline bool
scope_is_valid(const char *scope)
{
return pcmk__str_any_of(scope,
PCMK_XE_CONFIGURATION,
PCMK_XE_NODES,
PCMK_XE_RESOURCES,
PCMK_XE_CONSTRAINTS,
PCMK_XE_CRM_CONFIG,
PCMK_XE_RSC_DEFAULTS,
PCMK_XE_OP_DEFAULTS,
PCMK_XE_ACLS,
PCMK_XE_FENCING_TOPOLOGY,
PCMK_XE_TAGS,
PCMK_XE_ALERTS,
PCMK_XE_STATUS,
NULL);
}
+static int
+print_xml_id(xmlNode *xml, void *user_data)
+{
+ const char *id = pcmk__xe_id(xml);
+
+ if (id != NULL) {
+ printf("%s\n", id);
+ }
+ return pcmk_rc_ok;
+}
+
static gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
options.delete_all = false;
if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_UPGRADE;
} else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_QUERY;
} else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_ERASE;
} else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_BUMP;
} else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_CREATE;
} else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_MODIFY;
} else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH;
} else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_REPLACE;
} else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_DELETE;
} else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) {
options.cib_action = PCMK__CIB_REQUEST_DELETE;
options.delete_all = true;
} else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) {
options.cib_action = "empty";
pcmk__str_update(&options.validate_with, optarg);
} else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) {
options.cib_action = "md5-sum";
} else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned",
NULL)) {
options.cib_action = "md5-sum-versioned";
} else {
// Should be impossible
return FALSE;
}
return TRUE;
}
static gboolean
show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) {
options.acl_render_mode = pcmk__acl_render_default;
} else if (g_strcmp0(optarg, "namespace") == 0) {
options.acl_render_mode = pcmk__acl_render_namespace;
} else if (g_strcmp0(optarg, "text") == 0) {
options.acl_render_mode = pcmk__acl_render_text;
} else if (g_strcmp0(optarg, "color") == 0) {
options.acl_render_mode = pcmk__acl_render_color;
} else {
g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
"Invalid value '%s' for option '%s'",
optarg, option_name);
return FALSE;
}
return TRUE;
}
static gboolean
section_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) {
options.section_type = cibadmin_section_scope;
} else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) {
options.section_type = cibadmin_section_xpath;
} else {
// Should be impossible
return FALSE;
}
pcmk__str_update(&options.cib_section, optarg);
return TRUE;
}
static GOptionEntry command_entries[] = {
{ "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Upgrade the configuration to the latest syntax", NULL },
{ "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Query the contents of the CIB", NULL },
{ "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Erase the contents of the whole CIB", NULL },
{ "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Increase the CIB's epoch value by 1", NULL },
{ "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Create an object in the CIB (will fail if object already exists)",
NULL },
{ "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Find object somewhere in CIB's XML tree and update it (fails if object "
"does not exist unless -c is also specified)",
NULL },
{ "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Supply an update in the form of an XML diff (see crm_diff(8))", NULL },
{ "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Recursively replace an object in the CIB", NULL },
{ "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Delete first object matching supplied criteria (for example, "
"<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" "
PCMK_XA_NAME "=\"monitor\"/>).\n"
INDENT "The XML element name and all attributes must match in order for "
"the element to be deleted.",
NULL },
{ "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"When used with --xpath, remove all matching objects in the "
"configuration instead of just the first one",
NULL },
{ "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"Output an empty CIB. Accepts an optional schema name argument to use as "
"the " PCMK_XA_VALIDATE_WITH " value.\n"
INDENT "If no schema is given, the latest will be used.",
"[schema]" },
{ "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Calculate the on-disk CIB digest", NULL },
{ "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb, "Calculate an on-the-wire versioned CIB digest", NULL },
{ NULL }
};
static GOptionEntry data_entries[] = {
// @COMPAT These arguments should be last-one-wins
{ "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
&options.input_file,
"Retrieve XML from the named file. Currently this takes precedence\n"
INDENT "over --xml-text and --xml-pipe. In a future release, the last\n"
INDENT "one specified will be used.",
"FILE" },
{ "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
&options.input_string,
"Retrieve XML from the supplied string. Currently this takes precedence\n"
INDENT "over --xml-pipe, but --xml-file overrides this. In a future\n"
INDENT "release, the last one specified will be used.",
"STRING" },
{ "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.input_stdin,
"Retrieve XML from stdin. Currently --xml-file and --xml-text override\n"
INDENT "this. In a future release, the last one specified will be used.",
NULL },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
"Force the action to be performed", NULL },
{ "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT,
&options.message_timeout_sec,
"Time (in seconds) to wait before declaring the operation failed",
"value" },
{ "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user,
"Run the command with permissions of the named user (valid only for the "
"root and " CRM_DAEMON_USER " accounts)", "value" },
{ "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
"Limit scope of operation to specific section of CIB\n"
INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES
", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS
", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n"
INDENT " " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS
", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS
", " PCMK_XE_STATUS "\n"
INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
"appear takes effect",
"value" },
{ "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb,
"A valid XPath to use instead of --scope/-o\n"
INDENT "If both --scope/-o and --xpath/-a are specified, the last one to "
"appear takes effect",
"value" },
{ "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.get_node_path,
"When performing XPath queries, return paths of any matches found\n"
INDENT "(for example, "
"\"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION
"/" PCMK_XE_RESOURCES "/" PCMK_XE_CLONE
"[@" PCMK_XA_ID "='dummy-clone']"
"/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='dummy']\")",
NULL },
{ "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
show_access_cb,
"Whether to use syntax highlighting for ACLs (with -Q/--query and "
"-U/--user)\n"
INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, "
"default for non-terminal),\n"
INDENT " 'namespace', or 'auto' (use default value)\n"
INDENT "Default value: 'auto'",
"[value]" },
{ "score", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.score_update,
"Treat new attribute values as atomic score updates where possible "
"(with --modify/-M).\n"
INDENT "This currently happens by default and cannot be disabled, but\n"
INDENT "this default behavior is deprecated and will be removed in a\n"
INDENT "future release. Set this flag if this behavior is desired.\n"
INDENT "This option takes effect when updating XML attributes. For an\n"
INDENT "attribute named \"name\", if the new value is \"name++\" or\n"
INDENT "\"name+=X\" for some score X, the new value is set as follows:\n"
INDENT "If attribute \"name\" is not already set to some value in\n"
INDENT "the element being updated, the new value is set as a literal\n"
INDENT "string.\n"
INDENT "If the new value is \"name++\", then the attribute is set to \n"
INDENT "its existing value (parsed as a score) plus 1.\n"
INDENT "If the new value is \"name+=X\" for some score X, then the\n"
INDENT "attribute is set to its existing value plus X, where the\n"
INDENT "existing value and X are parsed and added as scores.\n"
INDENT "Scores are integer values capped at INFINITY and -INFINITY.\n"
INDENT "Refer to Pacemaker Explained for more details on scores,\n"
INDENT "including how they are parsed and added.",
NULL },
{ "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.allow_create,
"(Advanced) Allow target of --modify/-M to be created if it does not "
"exist",
NULL },
{ "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.no_children,
"(Advanced) When querying an object, do not include its children in the "
"result",
NULL },
{ "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node,
"(Advanced) Send command to the specified host", "value" },
// @COMPAT Deprecated since 3.0.0
{ "local", 'l', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.local,
"(deprecated)", NULL },
// @COMPAT Deprecated since 3.0.1
{ "sync-call", 's', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE,
&options.sync_call, "(deprecated)", NULL },
{ NULL }
};
static GOptionContext *
build_arg_context(pcmk__common_args_t *args)
{
const char *desc = NULL;
GOptionContext *context = NULL;
desc = "Examples:\n\n"
"Query the configuration:\n\n"
"\t# cibadmin --query\n\n"
"or just:\n\n"
"\t# cibadmin\n\n"
"Query just the cluster options configuration:\n\n"
"\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n"
"Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n"
"\t# cibadmin --query --xpath "
"\"//" PCMK_XE_NVPAIR
"[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n"
"Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n"
"\t# cibadmin --delete-all --xpath "
"\"//" PCMK_XE_NVPAIR
"[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n"
"Remove the resource named 'old':\n\n"
"\t# cibadmin --delete --xml-text "
"'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n"
"Remove all resources from the configuration:\n\n"
"\t# cibadmin --replace --scope " PCMK_XE_RESOURCES
" --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n"
"Replace complete configuration with contents of "
"$HOME/pacemaker.xml:\n\n"
"\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n"
"Replace " PCMK_XE_CONSTRAINTS " section of configuration with "
"contents of $HOME/constraints.xml:\n\n"
"\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS
" --xml-file $HOME/constraints.xml\n\n"
"Increase configuration version to prevent old configurations from "
"being loaded accidentally:\n\n"
"\t# cibadmin --modify --score --xml-text "
"'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH
"=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n"
"Edit the configuration with your favorite $EDITOR:\n\n"
"\t# cibadmin --query > $HOME/local.xml\n\n"
"\t# $EDITOR $HOME/local.xml\n\n"
"\t# cibadmin --replace --xml-file $HOME/local.xml\n\n"
"Assuming terminal, render configuration in color (green for "
"writable, blue for readable, red for\n"
"denied) to visualize permissions for user tony:\n\n"
"\t# cibadmin --show-access=color --query --user tony | less -r\n\n"
"SEE ALSO:\n"
" crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n";
context = pcmk__build_arg_context(args, NULL, NULL, "[]");
g_option_context_set_description(context, desc);
pcmk__add_arg_group(context, "commands", "Commands:", "Show command help",
command_entries);
pcmk__add_arg_group(context, "data", "Data:", "Show data help",
data_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
int rc = pcmk_rc_ok;
xmlNode *output = NULL;
xmlNode *input = NULL;
gchar *acl_cred = NULL;
GError *error = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx");
GOptionContext *context = build_arg_context(args);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
if (g_strv_length(processed_args) > 1) {
gchar *extra = g_strjoinv(" ", processed_args + 1);
gchar *help = g_option_context_get_help(context, TRUE, NULL);
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"non-option ARGV-elements: %s\n\n%s", extra, help);
g_free(extra);
g_free(help);
goto done;
}
if (args->version) {
g_strfreev(processed_args);
pcmk__free_arg_context(context);
/* FIXME: When cibadmin is converted to use formatted output, this can
* be replaced by out->version.
*/
pcmk__cli_help();
}
/* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like
*
* (func@file:line) error: CIB failures
*
* In cibadmin we explicitly output the XML portion without the prefixes. So
* we default to LOG_CRIT.
*/
pcmk__cli_init_logging("cibadmin", 0);
set_crm_log_level(LOG_CRIT);
if (args->verbosity > 0) {
cib__set_call_options(options.cmd_options, crm_system_name,
cib_verbose);
for (int i = 0; i < args->verbosity; i++) {
crm_bump_log_level(argc, argv);
}
}
pcmk__assert(options.cib_action != NULL);
if (strcmp(options.cib_action, "empty") == 0) {
// Output an empty CIB
GString *buf = g_string_sized_new(1024);
output = createEmptyCib(1);
crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
pcmk__xml_string(output, pcmk__xml_fmt_pretty, buf, 0);
fprintf(stdout, "%s", buf->str);
g_string_free(buf, TRUE);
goto done;
}
if (cib_action_is_dangerous() && !options.force) {
exit_code = CRM_EX_UNSAFE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"The supplied command is considered dangerous. To prevent "
"accidental destruction of the cluster, the --force flag "
"is required in order to proceed.");
goto done;
}
- if (options.message_timeout_sec < 1) {
- // Set default timeout
- options.message_timeout_sec = 30;
- }
-
if (options.section_type == cibadmin_section_xpath) {
// Enable getting section by XPath
cib__set_call_options(options.cmd_options, crm_system_name,
cib_xpath);
} else if (options.section_type == cibadmin_section_scope) {
if (!scope_is_valid(options.cib_section)) {
// @COMPAT: Consider requiring --force to proceed
fprintf(stderr,
"Invalid value '%s' for '--scope'. Operation will apply "
"to the entire CIB.\n", options.cib_section);
}
}
if (options.allow_create) {
// Allow target of --modify/-M to be created if it does not exist
cib__set_call_options(options.cmd_options, crm_system_name,
cib_can_create);
}
if (options.delete_all) {
// With cibadmin_section_xpath, remove all matching objects
cib__set_call_options(options.cmd_options, crm_system_name,
cib_multiple);
}
if (options.get_node_path) {
/* Enable getting node path of XPath query matches.
* Meaningful only if options.section_type == cibadmin_section_xpath.
*/
cib__set_call_options(options.cmd_options, crm_system_name,
cib_xpath_address);
}
if (options.no_children) {
// When querying an object, don't include its children in the result
cib__set_call_options(options.cmd_options, crm_system_name,
cib_no_children);
}
if (read_input(&input, &error) != pcmk_rc_ok) {
goto done;
}
/* @TODO Since this was added by 99f414d, we have not entered this ACL
* render setup section if any input was provided. Is that correct?
*/
if ((input == NULL) && (options.acl_render_mode != pcmk__acl_render_none)) {
char *username = NULL;
if (options.cib_user == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"The supplied command requires -U user specified.");
goto done;
}
// @COMPAT Fail if pcmk_acl_required(username)
username = pcmk__uid2username(geteuid());
if (pcmk_acl_required(username)) {
fprintf(stderr,
"Warning: cibadmin is being run as user %s, which is "
"subject to ACLs. As a result, ACLs for user %s may be "
"incorrect or incomplete in the output. In a future "
"release, running as a privileged user (root or "
CRM_DAEMON_USER ") will be required for --show-access.\n",
username, options.cib_user);
}
free(username);
/* Note: acl_cred takes ownership of options.cib_user here.
* options.cib_user is set to NULL so that the CIB is obtained as the
* user running the cibadmin command. The CIB must be obtained as a user
* with full permissions in order to show the CIB correctly annotated
* for the options.cib_user's permissions.
*/
acl_cred = options.cib_user;
options.cib_user = NULL;
}
- if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_casei)) {
- char *digest = NULL;
-
- if (input == NULL) {
- exit_code = CRM_EX_USAGE;
- g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
- "Please supply XML to process with -X, -x, or -p");
- goto done;
- }
-
- digest = pcmk__digest_on_disk_cib(input);
- fprintf(stderr, "Digest: ");
- fprintf(stdout, "%s\n", pcmk__s(digest, ""));
- free(digest);
+ if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_none)) {
+ output_digest(input, true, &error);
goto done;
-
- } else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) {
- char *digest = NULL;
-
- if (input == NULL) {
- exit_code = CRM_EX_USAGE;
- g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
- "Please supply XML to process with -X, -x, or -p");
- goto done;
- }
-
- digest = pcmk__digest_xml(input, true);
- fprintf(stdout, "%s\n", pcmk__s(digest, ""));
- free(digest);
+ }
+ if (pcmk__str_eq(options.cib_action, "md5-sum-versioned", pcmk__str_none)) {
+ output_digest(input, false, &error);
goto done;
+ }
- } else if (pcmk__str_eq(options.cib_action, PCMK__CIB_REQUEST_MODIFY,
- pcmk__str_none)) {
+ if (pcmk__str_eq(options.cib_action, PCMK__CIB_REQUEST_MODIFY,
+ pcmk__str_none)) {
/* @COMPAT When we drop default support for expansion in cibadmin, guard
* with `if (options.score_update)`
*/
cib__set_call_options(options.cmd_options, crm_system_name,
cib_score_update);
}
- rc = do_init();
- if (rc != pcmk_ok) {
- rc = pcmk_legacy2rc(rc);
+ rc = cib__create_signon(&cib_conn);
+ if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
-
- crm_err("Init failed, could not perform requested operations: %s",
- pcmk_rc_str(rc));
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
- "Init failed, could not perform requested operations: %s",
- pcmk_rc_str(rc));
+ "Could not connect to the CIB API: %s", pcmk_rc_str(rc));
goto done;
}
- rc = do_work(input, &output);
+ cib_conn->call_timeout = options.message_timeout_sec;
+ if (cib_conn->call_timeout < 1) {
+ fprintf(stderr, "Timeout must be positive, defaulting to 30\n");
+ cib_conn->call_timeout = 30;
+ }
+
+ if (pcmk__str_eq(options.cib_action, PCMK__CIB_REQUEST_REPLACE,
+ pcmk__str_none)
+ && pcmk__xe_is(input, PCMK_XE_CIB)) {
+
+ xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS);
+
+ if (status == NULL) {
+ pcmk__xe_create(input, PCMK_XE_STATUS);
+ }
+ }
+
+ rc = cib_internal_op(cib_conn, options.cib_action, options.dest_node,
+ options.cib_section, input, &output,
+ options.cmd_options, options.cib_user);
rc = pcmk_legacy2rc(rc);
if ((rc == pcmk_rc_schema_unchanged)
&& (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0)) {
- report_schema_unchanged();
+ printf("Upgrade unnecessary: %s\n", pcmk_rc_str(rc));
+ exit_code = CRM_EX_OK;
} else if (rc != pcmk_rc_ok) {
- crm_err("Call failed: %s", pcmk_rc_str(rc));
fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc));
exit_code = pcmk_rc2exitc(rc);
if (rc == pcmk_rc_schema_validation) {
if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) {
xmlNode *obj = NULL;
- if (the_cib->cmds->query(the_cib, NULL, &obj,
- options.cmd_options) == pcmk_ok) {
+ if (cib_conn->cmds->query(cib_conn, NULL, &obj,
+ options.cmd_options) == pcmk_ok) {
pcmk__update_schema(&obj, NULL, true, false);
}
pcmk__xml_free(obj);
} else if (output != NULL) {
// Show validation errors to stderr
pcmk__validate_xml(output, NULL, NULL, NULL);
}
}
}
if (output == NULL) {
goto done;
}
if (options.acl_render_mode != pcmk__acl_render_none) {
xmlDoc *acl_evaled_doc = NULL;
xmlChar *rendered = NULL;
rc = pcmk__acl_annotate_permissions(acl_cred, output->doc,
&acl_evaled_doc);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_CONFIG;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not evaluate access per request (%s, error: %s)",
acl_cred, pcmk_rc_str(rc));
goto done;
}
rc = pcmk__acl_evaled_render(acl_evaled_doc, options.acl_render_mode,
&rendered);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_CONFIG;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not render evaluated access: %s",
pcmk_rc_str(rc));
goto done;
}
printf("%s\n", (char *) rendered);
xmlFree(rendered);
+ } else if (pcmk_is_set(options.cmd_options, cib_xpath_address)
+ && pcmk__xe_is(output, PCMK__XE_XPATH_QUERY)) {
+
+ pcmk__xe_foreach_child(output, PCMK__XE_XPATH_QUERY_PATH, print_xml_id,
+ NULL);
+
} else {
- print_xml_output(output);
+ GString *buf = g_string_sized_new(1024);
+
+ pcmk__xml_string(output, pcmk__xml_fmt_pretty, buf, 0);
+
+ printf("%s", buf->str);
+ g_string_free(buf, TRUE);
}
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
g_free(options.cib_user);
g_free(options.dest_node);
g_free(options.input_file);
g_free(options.input_string);
free(options.cib_section);
free(options.validate_with);
g_free(acl_cred);
pcmk__xml_free(input);
pcmk__xml_free(output);
- rc = cib__clean_up_connection(&the_cib);
+ rc = cib__clean_up_connection(&cib_conn);
if (exit_code == CRM_EX_OK) {
exit_code = pcmk_rc2exitc(rc);
}
pcmk__output_and_clear_error(&error, NULL);
crm_exit(exit_code);
}
-
-static int
-do_work(xmlNode *input, xmlNode **output)
-{
- /* construct the request */
- the_cib->call_timeout = options.message_timeout_sec;
- if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0)
- && pcmk__xe_is(input, PCMK_XE_CIB)) {
- xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS);
-
- if (status == NULL) {
- pcmk__xe_create(input, PCMK_XE_STATUS);
- }
- }
-
- crm_trace("Passing \"%s\" to variant_op...", options.cib_action);
- return cib_internal_op(the_cib, options.cib_action, options.dest_node,
- options.cib_section, input, output,
- options.cmd_options, options.cib_user);
-}
-
-int
-do_init(void)
-{
- int rc = pcmk_ok;
-
- the_cib = cib_new();
- rc = cib__signon_attempts(the_cib, cib_command, 5);
- if (rc != pcmk_ok) {
- crm_err("Could not connect to the CIB: %s", pcmk_strerror(rc));
- fprintf(stderr, "Could not connect to the CIB: %s\n",
- pcmk_strerror(rc));
- }
-
- return rc;
-}
diff --git a/tools/crm_attribute.c b/tools/crm_attribute.c
index 0bbbba6a46..38d8cfad58 100644
--- a/tools/crm_attribute.c
+++ b/tools/crm_attribute.c
@@ -1,992 +1,989 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SUMMARY "crm_attribute - query and update Pacemaker cluster options and node attributes"
enum attr_cmd {
attr_cmd_none,
attr_cmd_delete,
attr_cmd_list,
attr_cmd_query,
attr_cmd_update,
};
GError *error = NULL;
crm_exit_t exit_code = CRM_EX_OK;
uint64_t cib_opts = cib_sync_call;
static pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
struct {
enum attr_cmd command;
gchar *attr_default;
gchar *attr_id;
gchar *attr_name;
uint32_t attr_options;
gchar *attr_pattern;
char *attr_value;
char *dest_node;
gchar *dest_uname;
gboolean inhibit;
gchar *set_name;
char *set_type;
gchar *type;
char *opt_list;
gboolean all;
bool promotion_score;
gboolean score_update;
} options = {
.command = attr_cmd_query,
};
#define INDENT " "
static gboolean
list_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error) {
options.command = attr_cmd_list;
pcmk__str_update(&options.opt_list, optarg);
return TRUE;
}
static gboolean
delete_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.command = attr_cmd_delete;
pcmk__str_update(&options.attr_value, NULL);
return TRUE;
}
static gboolean
attr_name_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
options.promotion_score = false;
if (options.attr_name != NULL) {
g_free(options.attr_name);
}
options.attr_name = g_strdup(optarg);
return TRUE;
}
static gboolean
promotion_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
char *score_name = NULL;
options.promotion_score = true;
if (options.attr_name) {
g_free(options.attr_name);
}
score_name = pcmk_promotion_score_name(optarg);
if (score_name != NULL) {
options.attr_name = g_strdup(score_name);
free(score_name);
} else {
options.attr_name = NULL;
}
return TRUE;
}
static gboolean
update_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.command = attr_cmd_update;
pcmk__str_update(&options.attr_value, optarg);
return TRUE;
}
static gboolean
utilization_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
if (options.type) {
g_free(options.type);
}
options.type = g_strdup(PCMK_XE_NODES);
pcmk__str_update(&options.set_type, PCMK_XE_UTILIZATION);
return TRUE;
}
static gboolean
value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.command = attr_cmd_query;
pcmk__str_update(&options.attr_value, NULL);
return TRUE;
}
static gboolean
wait_cb (const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
if (pcmk__str_eq(optarg, "no", pcmk__str_none)) {
pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
return TRUE;
} else if (pcmk__str_eq(optarg, PCMK__VALUE_LOCAL, pcmk__str_none)) {
pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local);
return TRUE;
} else if (pcmk__str_eq(optarg, PCMK__VALUE_CLUSTER, pcmk__str_none)) {
pcmk__clear_node_attr_flags(options.attr_options, pcmk__node_attr_sync_local | pcmk__node_attr_sync_cluster);
pcmk__set_node_attr_flags(options.attr_options, pcmk__node_attr_sync_cluster);
return TRUE;
} else {
g_set_error(err, PCMK__EXITC_ERROR, CRM_EX_USAGE,
"--wait= must be one of 'no', 'local', 'cluster'");
return FALSE;
}
}
static GOptionEntry selecting_entries[] = {
{ "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.all,
"With -L/--list-options, include advanced and deprecated options in the\n"
INDENT "output. This is always treated as true when --output-as=xml is\n"
INDENT "specified.",
NULL,
},
{ "id", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id,
"(Advanced) Operate on instance of specified attribute with this\n"
INDENT "XML ID",
"XML_ID"
},
{ "name", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, attr_name_cb,
"Operate on attribute or option with this name. For queries, this\n"
INDENT "is optional, in which case all matching attributes will be\n"
INDENT "returned.",
"NAME"
},
{ "pattern", 'P', 0, G_OPTION_ARG_STRING, &options.attr_pattern,
"Operate on all attributes matching this pattern\n"
INDENT "(with -v, -D, or -G)",
"PATTERN"
},
{ "promotion", 'p', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, promotion_cb,
"Operate on node attribute used as promotion score for specified\n"
INDENT "resource, or resource given in OCF_RESOURCE_INSTANCE environment\n"
INDENT "variable if none is specified; this also defaults -l/--lifetime\n"
INDENT "to reboot (normally invoked from an OCF resource agent)",
"RESOURCE"
},
{ "set-name", 's', 0, G_OPTION_ARG_STRING, &options.set_name,
"(Advanced) Operate on instance of specified attribute that is\n"
INDENT "within set with this XML ID",
"NAME"
},
{ NULL }
};
static GOptionEntry command_entries[] = {
{ "list-options", 'L', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, list_cb,
"List all available options of the given type.\n"
INDENT "Allowed values: " PCMK__VALUE_CLUSTER,
"TYPE"
},
{ "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, delete_cb,
"Delete the attribute/option (with -n or -P)",
NULL
},
{ "query", 'G', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, value_cb,
"Query the current value of the attribute/option.\n"
INDENT "See also: -n, -P",
NULL
},
{ "update", 'v', 0, G_OPTION_ARG_CALLBACK, update_cb,
"Update the value of the attribute/option (with -n or -P)",
"VALUE"
},
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default,
"(Advanced) Default value to display if none is found in configuration",
"VALUE"
},
{ "lifetime", 'l', 0, G_OPTION_ARG_STRING, &options.type,
"Lifetime of the node attribute.\n"
INDENT "Valid values: reboot, forever",
"LIFETIME"
},
{ "node", 'N', 0, G_OPTION_ARG_STRING, &options.dest_uname,
"Set a node attribute for named node (instead of a cluster option).\n"
INDENT "See also: -l",
"NODE"
},
{ "type", 't', 0, G_OPTION_ARG_STRING, &options.type,
"Which part of the configuration to update/delete/query the option in.\n"
INDENT "Valid values: crm_config, rsc_defaults, op_defaults, tickets",
"SECTION"
},
{ "score", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.score_update,
"Treat new attribute values as atomic score updates where possible\n"
INDENT "(with --update/-v, when running against a CIB file or updating\n"
INDENT "an attribute outside the " PCMK_XE_STATUS " section; enabled\n"
INDENT "by default if --promotion/-p is specified)\n\n"
INDENT "This currently happens by default and cannot be disabled, but\n"
INDENT "this default behavior is deprecated and will be removed in a\n"
INDENT "future release (exception: this will remain the default with\n"
INDENT "--promotion/-p). Set this flag if this behavior is desired.\n\n"
INDENT "This option takes effect when updating XML attributes. For an\n"
INDENT "attribute named \"name\", if the new value is \"name++\" or\n"
INDENT "\"name+=X\" for some score X, the new value is set as follows:\n"
INDENT " * If attribute \"name\" is not already set to some value in\n"
INDENT " the element being updated, the new value is set as a literal\n"
INDENT " string.\n"
INDENT " * If the new value is \"name++\", then the attribute is set to\n"
INDENT " its existing value (parsed as a score) plus 1.\n"
INDENT " * If the new value is \"name+=X\" for some score X, then the\n"
INDENT " attribute is set to its existing value plus X, where the\n"
INDENT " existing value and X are parsed and added as scores.\n\n"
INDENT "Scores are integer values capped at INFINITY and -INFINITY.\n"
INDENT "Refer to Pacemaker Explained for more details on scores,\n"
INDENT "including how they are parsed and added.",
NULL },
{ "wait", 'W', 0, G_OPTION_ARG_CALLBACK, wait_cb,
"Wait for some event to occur before returning. Values are 'no' (wait\n"
INDENT "only for the attribute daemon to acknowledge the request),\n"
INDENT "'local' (wait until the change has propagated to where a local\n"
INDENT "query will return the request value, or the value set by a\n"
INDENT "later request), or 'cluster' (wait until the change has propagated\n"
INDENT "to where a query anywhere on the cluster will return the requested\n"
INDENT "value, or the value set by a later request). Default is 'no'.\n"
INDENT "(with -N, and one of -D or -u)",
"UNTIL" },
{ "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, utilization_cb,
"Set an utilization attribute for the node.",
NULL
},
{ "inhibit-policy-engine", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.inhibit,
NULL, NULL
},
{ NULL }
};
static GOptionEntry deprecated_entries[] = {
{ "attr-id", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.attr_id,
NULL, NULL
},
// NOTE: resource-agents <4.2.0 (2018-10-24) uses this option
{ "attr-name", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, attr_name_cb,
NULL, NULL
},
{ "attr-value", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, update_cb,
NULL, NULL
},
{ "delete-attr", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, delete_cb,
NULL, NULL
},
// NOTE: resource-agents <4.2.0 (2018-10-24) uses this option
{ "get-value", 0, G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, value_cb,
NULL, NULL
},
// NOTE: resource-agents <4.2.0 (2018-10-24) uses this option
{ "node-uname", 'U', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.dest_uname,
NULL, NULL
},
{ NULL }
};
static void
get_node_name_from_local(void)
{
struct utsname hostinfo;
g_free(options.dest_uname);
if (uname(&hostinfo) == 0) {
options.dest_uname = g_strdup(hostinfo.nodename);
} else {
options.dest_uname = NULL;
}
}
static int
send_attrd_update(enum attr_cmd command, const char *attr_node,
const char *attr_name, const char *attr_value,
const char *attr_set, const char *attr_dampen,
uint32_t attr_options)
{
int rc = pcmk_rc_ok;
uint32_t opts = attr_options;
switch (command) {
case attr_cmd_delete:
rc = pcmk__attrd_api_delete(NULL, attr_node, attr_name, opts);
break;
case attr_cmd_update:
rc = pcmk__attrd_api_update(NULL, attr_node, attr_name,
attr_value, NULL, attr_set, NULL,
opts | pcmk__node_attr_value);
break;
default:
break;
}
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc, "Could not update %s=%s: %s (%d)",
attr_name, attr_value, pcmk_rc_str(rc), rc);
}
return rc;
}
struct delete_data_s {
pcmk__output_t *out;
cib_t *cib;
};
static int
delete_attr_on_node(xmlNode *child, void *userdata)
{
struct delete_data_s *dd = (struct delete_data_s *) userdata;
const char *attr_name = crm_element_value(child, PCMK_XA_NAME);
int rc = pcmk_rc_ok;
if (!pcmk__str_eq(attr_name, options.attr_pattern, pcmk__str_regex)) {
return pcmk_rc_ok;
}
rc = cib__delete_node_attr(dd->out, dd->cib, cib_opts, options.type,
options.dest_node, options.set_type,
options.set_name, options.attr_id,
attr_name, options.attr_value, NULL);
if (rc == ENXIO) {
rc = pcmk_rc_ok;
}
return rc;
}
static void
command_list(pcmk__output_t *out)
{
if (pcmk__str_eq(options.opt_list, PCMK__VALUE_CLUSTER, pcmk__str_none)) {
exit_code = pcmk_rc2exitc(pcmk__list_cluster_options(out, options.all));
} else {
// @TODO Improve usage messages to reduce duplication
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
"Invalid --list-options value '%s'. Allowed values: "
PCMK__VALUE_CLUSTER,
pcmk__s(options.opt_list, "(BUG: none)"));
}
}
static int
command_delete(pcmk__output_t *out, cib_t *cib)
{
int rc = pcmk_rc_ok;
xmlNode *result = NULL;
bool use_pattern = options.attr_pattern != NULL;
/* See the comment in command_query regarding xpath and regular expressions. */
if (use_pattern) {
struct delete_data_s dd = { out, cib };
rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
options.set_type, options.set_name, NULL, NULL,
NULL, &result);
if (rc != pcmk_rc_ok) {
goto done_deleting;
}
rc = pcmk__xe_foreach_child(result, NULL, delete_attr_on_node, &dd);
} else {
rc = cib__delete_node_attr(out, cib, cib_opts, options.type, options.dest_node,
options.set_type, options.set_name, options.attr_id,
options.attr_name, options.attr_value, NULL);
}
done_deleting:
pcmk__xml_free(result);
if (rc == ENXIO) {
/* Nothing to delete...
* which means it's not there...
* which is what the admin wanted
*/
rc = pcmk_rc_ok;
}
return rc;
}
struct update_data_s {
pcmk__output_t *out;
cib_t *cib;
int is_remote_node;
};
static int
update_attr_on_node(xmlNode *child, void *userdata)
{
struct update_data_s *ud = (struct update_data_s *) userdata;
const char *attr_name = crm_element_value(child, PCMK_XA_NAME);
if (!pcmk__str_eq(attr_name, options.attr_pattern, pcmk__str_regex)) {
return pcmk_rc_ok;
}
return cib__update_node_attr(ud->out, ud->cib, cib_opts, options.type,
options.dest_node, options.set_type,
options.set_name, options.attr_id,
attr_name, options.attr_value, NULL,
ud->is_remote_node? PCMK_VALUE_REMOTE : NULL);
}
static int
command_update(pcmk__output_t *out, cib_t *cib, int is_remote_node)
{
int rc = pcmk_rc_ok;
xmlNode *result = NULL;
bool use_pattern = options.attr_pattern != NULL;
/* @COMPAT When we drop default support for expansion in crm_attribute,
* guard with `if (options.score_update)`
*/
cib__set_call_options(cib_opts, crm_system_name, cib_score_update);
/* See the comment in command_query regarding xpath and regular expressions. */
if (use_pattern) {
struct update_data_s ud = { out, cib, is_remote_node };
rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
options.set_type, options.set_name, NULL, NULL,
NULL, &result);
if (rc != pcmk_rc_ok) {
goto done_updating;
}
rc = pcmk__xe_foreach_child(result, NULL, update_attr_on_node, &ud);
} else {
rc = cib__update_node_attr(out, cib, cib_opts, options.type,
options.dest_node, options.set_type,
options.set_name, options.attr_id,
options.attr_name, options.attr_value, NULL,
is_remote_node? PCMK_VALUE_REMOTE : NULL);
}
done_updating:
pcmk__xml_free(result);
return rc;
}
struct output_data_s {
pcmk__output_t *out;
bool use_pattern;
bool did_output;
};
static int
output_one_attribute(xmlNode *node, void *userdata)
{
struct output_data_s *od = (struct output_data_s *) userdata;
const char *name = crm_element_value(node, PCMK_XA_NAME);
const char *value = crm_element_value(node, PCMK_XA_VALUE);
const char *type = options.type;
const char *attr_id = options.attr_id;
if (od->use_pattern && !pcmk__str_eq(name, options.attr_pattern, pcmk__str_regex)) {
return pcmk_rc_ok;
}
od->out->message(od->out, "attribute", type, attr_id, name, value, NULL,
od->out->quiet, true);
od->did_output = true;
crm_info("Read %s='%s' %s%s",
pcmk__s(name, ""), pcmk__s(value, ""),
options.set_name ? "in " : "", options.set_name ? options.set_name : "");
return pcmk_rc_ok;
}
static int
command_query(pcmk__output_t *out, cib_t *cib)
{
int rc = pcmk_rc_ok;
xmlNode *result = NULL;
bool use_pattern = options.attr_pattern != NULL;
/* libxml2 doesn't support regular expressions in xpath queries (which is how
* cib__get_node_attrs -> find_attr finds attributes). So instead, we'll just
* find all the attributes for a given node here by passing NULL for attr_id
* and attr_name, and then later see if they match the given pattern.
*/
if (use_pattern) {
rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
options.set_type, options.set_name, NULL,
NULL, NULL, &result);
} else {
rc = cib__get_node_attrs(out, cib, options.type, options.dest_node,
options.set_type, options.set_name, options.attr_id,
options.attr_name, NULL, &result);
}
if (rc == ENXIO && options.attr_default) {
/* Make static analysis happy */
const char *type = options.type;
const char *attr_id = options.attr_id;
const char *attr_name = options.attr_name;
const char *attr_default = options.attr_default;
out->message(out, "attribute", type, attr_id, attr_name, attr_default,
NULL, out->quiet, true);
rc = pcmk_rc_ok;
} else if (rc != pcmk_rc_ok) {
// Don't do anything.
} else if (result->children != NULL) {
struct output_data_s od = { out, use_pattern, false };
pcmk__xe_foreach_child(result, NULL, output_one_attribute, &od);
if (!od.did_output) {
rc = ENXIO;
}
} else {
struct output_data_s od = { out, use_pattern, false };
output_one_attribute(result, &od);
}
pcmk__xml_free(result);
return rc;
}
static void
set_type(void)
{
if (options.type == NULL) {
if (options.promotion_score) {
// Updating a promotion score node attribute
options.type = g_strdup(PCMK_XE_STATUS);
} else if (options.dest_uname != NULL) {
// Updating some other node attribute
options.type = g_strdup(PCMK_XE_NODES);
} else {
// Updating cluster options
options.type = g_strdup(PCMK_XE_CRM_CONFIG);
}
} else if (pcmk__str_eq(options.type, PCMK_VALUE_REBOOT, pcmk__str_casei)) {
options.type = g_strdup(PCMK_XE_STATUS);
} else if (pcmk__str_eq(options.type, "forever", pcmk__str_casei)) {
options.type = g_strdup(PCMK_XE_NODES);
}
}
static bool
use_attrd(void)
{
/* Only go through the attribute manager for transient attributes, and
* then only if we're not using a file as the CIB.
*/
return pcmk__str_eq(options.type, PCMK_XE_STATUS, pcmk__str_casei) &&
getenv("CIB_file") == NULL && getenv("CIB_shadow") == NULL;
}
static bool
try_ipc_update(void)
{
return use_attrd()
&& ((options.command == attr_cmd_delete)
|| (options.command == attr_cmd_update));
}
static bool
pattern_used_correctly(void)
{
/* --pattern can only be used with:
* -G (query), -v (update), or -D (delete)
*/
switch (options.command) {
case attr_cmd_delete:
case attr_cmd_query:
case attr_cmd_update:
return true;
default:
return false;
}
}
static bool
delete_used_correctly(void)
{
return (options.command != attr_cmd_delete)
|| (options.attr_name != NULL)
|| (options.attr_pattern != NULL);
}
static bool
update_used_correctly(void)
{
return (options.command != attr_cmd_update)
|| (options.attr_name != NULL)
|| (options.attr_pattern != NULL);
}
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
GOptionContext *context = NULL;
GOptionEntry extra_prog_entries[] = {
{ "quiet", 'q', 0, G_OPTION_ARG_NONE, &(args->quiet),
"Print only the value on stdout",
NULL },
// NOTE: resource-agents <4.2.0 (2018-10-24) uses -Q
{ "quiet", 'Q', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &(args->quiet),
NULL, NULL
},
{ NULL }
};
const char *description = "Examples:\n\n"
"Add new node attribute called 'location' with the value of 'office' for host 'myhost':\n\n"
"\tcrm_attribute --node myhost --name location --update office\n\n"
"Query the value of the 'location' node attribute for host 'myhost':\n\n"
"\tcrm_attribute --node myhost --name location --query\n\n"
"Change the value of the 'location' node attribute for host 'myhost':\n\n"
"\tcrm_attribute --node myhost --name location --update backoffice\n\n"
"Delete the 'location' node attribute for host 'myhost':\n\n"
"\tcrm_attribute --node myhost --name location --delete\n\n"
"Query the value of the '" PCMK_OPT_CLUSTER_DELAY
"' cluster option:\n\n"
"\tcrm_attribute --type crm_config --name "
PCMK_OPT_CLUSTER_DELAY " --query\n\n"
"Query value of the '" PCMK_OPT_CLUSTER_DELAY
"' cluster option and print only the value:\n\n"
"\tcrm_attribute --type crm_config --name "
PCMK_OPT_CLUSTER_DELAY " --query --quiet\n\n";
context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
pcmk__add_main_args(context, extra_prog_entries);
g_option_context_set_description(context, description);
pcmk__add_arg_group(context, "selections", "Selecting attributes:",
"Show selecting options", selecting_entries);
pcmk__add_arg_group(context, "command", "Commands:",
"Show command options", command_entries);
pcmk__add_arg_group(context, "additional", "Additional options:",
"Show additional options", addl_entries);
pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
"Show deprecated options", deprecated_entries);
return context;
}
int
main(int argc, char **argv)
{
cib_t *the_cib = NULL;
int is_remote_node = 0;
int rc = pcmk_rc_ok;
pcmk__output_t *out = NULL;
GOptionGroup *output_group = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "NPUdilnpstv");
GOptionContext *context = build_arg_context(args, &output_group);
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
pcmk__cli_init_logging("crm_attribute", args->verbosity);
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_ERROR;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s",
args->output_ty, pcmk_rc_str(rc));
goto done;
}
pcmk__register_lib_messages(out);
if (args->version) {
out->version(out);
goto done;
}
out->quiet = args->quiet;
if (options.command == attr_cmd_list) {
command_list(out);
goto done;
}
if (options.promotion_score && options.attr_name == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"-p/--promotion must be called from an OCF resource agent "
"or with a resource ID specified");
goto done;
}
if (options.inhibit) {
crm_warn("Inhibiting notifications for this update");
cib__set_call_options(cib_opts, crm_system_name, cib_inhibit_notify);
}
- the_cib = cib_new();
- rc = cib__signon_attempts(the_cib, cib_command, 5);
- rc = pcmk_legacy2rc(rc);
-
+ rc = cib__create_signon(&the_cib);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not connect to the CIB: %s", pcmk_rc_str(rc));
goto done;
}
set_type();
// Use default node if not given (except for cluster options and tickets)
if (!pcmk__strcase_any_of(options.type,
PCMK_XE_CRM_CONFIG, PCMK_XE_TICKETS, NULL)) {
/* If we are being called from a resource agent via the cluster,
* the correct local node name will be passed as an environment
* variable. Otherwise, we have to ask the cluster.
*/
const char *target = pcmk__node_attr_target(options.dest_uname);
if (target != NULL) {
/* If options.dest_uname is "auto" or "localhost", then
* pcmk__node_attr_target() may return it, depending on environment
* variables. In that case, attribute lookups will fail for "auto"
* (unless there's a node named "auto"). attrd maps "localhost" to
* the true local node name for queries.
*
* @TODO
* * Investigate whether "localhost" is mapped to a real node name
* for non-query commands. If not, possibly modify it so that it
* is.
* * Map "auto" to "localhost" (probably).
*/
if (target != (const char *) options.dest_uname) {
g_free(options.dest_uname);
options.dest_uname = g_strdup(target);
}
} else if (getenv("CIB_file") != NULL && options.dest_uname == NULL) {
get_node_name_from_local();
}
if (options.dest_uname == NULL) {
char *node_name = NULL;
rc = pcmk__query_node_name(out, 0, &node_name, 0);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
free(node_name);
goto done;
}
options.dest_uname = g_strdup(node_name);
free(node_name);
}
rc = query_node_uuid(the_cib, options.dest_uname, &options.dest_node, &is_remote_node);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not map name=%s to a UUID", options.dest_uname);
goto done;
}
}
if (!delete_used_correctly()) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error: must specify attribute name or pattern to delete");
goto done;
}
if (!update_used_correctly()) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error: must specify attribute name or pattern to update");
goto done;
}
if (options.attr_pattern) {
if (options.attr_name) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error: --name and --pattern cannot be used at the same time");
goto done;
}
if (!pattern_used_correctly()) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error: pattern can only be used with delete, query, or update");
goto done;
}
g_free(options.attr_name);
options.attr_name = options.attr_pattern;
options.attr_options |= pcmk__node_attr_pattern;
}
if (is_remote_node) {
options.attr_options |= pcmk__node_attr_remote;
}
if (pcmk__str_eq(options.set_type, PCMK_XE_UTILIZATION, pcmk__str_none)) {
options.attr_options |= pcmk__node_attr_utilization;
}
if (try_ipc_update() &&
(send_attrd_update(options.command, options.dest_uname, options.attr_name,
options.attr_value, options.set_name, NULL, options.attr_options) == pcmk_rc_ok)) {
const char *update = options.attr_value;
if (options.command == attr_cmd_delete) {
update = "";
}
crm_info("Update %s=%s sent to the attribute manager",
options.attr_name, update);
} else if (options.command == attr_cmd_delete) {
rc = command_delete(out, the_cib);
} else if (options.command == attr_cmd_update) {
rc = command_update(out, the_cib, is_remote_node);
} else {
rc = command_query(out, the_cib);
}
if (rc == ENOTUNIQ) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Please choose from one of the matches below and supply the 'id' with --attr-id");
} else if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error performing operation: %s", pcmk_rc_str(rc));
}
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
free(options.attr_default);
g_free(options.attr_id);
g_free(options.attr_name);
free(options.attr_value);
free(options.dest_node);
g_free(options.dest_uname);
g_free(options.set_name);
free(options.set_type);
g_free(options.type);
cib__clean_up_connection(&the_cib);
pcmk__output_and_clear_error(&error, out);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
pcmk__unregister_formats();
return crm_exit(exit_code);
}
diff --git a/tools/crm_node.c b/tools/crm_node.c
index 5333585766..dc1996d7ac 100644
--- a/tools/crm_node.c
+++ b/tools/crm_node.c
@@ -1,875 +1,871 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SUMMARY "crm_node - Tool for displaying low-level node information"
struct {
gboolean corosync;
gboolean dangerous_cmd;
gboolean force_flag;
char command;
int nodeid;
char *target_uname;
} options = {
.command = '\0',
.force_flag = FALSE
};
gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
gboolean name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
gboolean remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
static GError *error = NULL;
static GMainLoop *mainloop = NULL;
static crm_exit_t exit_code = CRM_EX_OK;
static pcmk__output_t *out = NULL;
#define INDENT " "
static GOptionEntry command_entries[] = {
{ "cluster-id", 'i', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display this node's cluster id",
NULL },
{ "list", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display all known members (past and present) of this cluster",
NULL },
{ "name", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the name used by the cluster for this node",
NULL },
{ "partition", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the members of this partition",
NULL },
{ "quorum", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display a 1 if our partition has quorum, 0 if not",
NULL },
{ "name-for-id", 'N', 0, G_OPTION_ARG_CALLBACK, name_cb,
"Display the name used by the cluster for the node with the specified ID",
"ID" },
{ "remove", 'R', 0, G_OPTION_ARG_CALLBACK, remove_cb,
"(Advanced) Remove the (stopped) node with the specified name from Pacemaker's\n"
INDENT "configuration and caches (the node must already have been removed from\n"
INDENT "the underlying cluster stack configuration",
"NAME" },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "force", 'f', 0, G_OPTION_ARG_NONE, &options.force_flag,
NULL,
NULL },
#if SUPPORT_COROSYNC
/* Unused and deprecated */
{ "corosync", 'C', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.corosync,
NULL,
NULL },
#endif
// @TODO add timeout option for when IPC replies are needed
{ NULL }
};
static pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
if (pcmk__str_eq("-i", option_name, pcmk__str_casei) || pcmk__str_eq("--cluster-id", option_name, pcmk__str_casei)) {
options.command = 'i';
} else if (pcmk__str_eq("-l", option_name, pcmk__str_casei) || pcmk__str_eq("--list", option_name, pcmk__str_casei)) {
options.command = 'l';
} else if (pcmk__str_eq("-n", option_name, pcmk__str_casei) || pcmk__str_eq("--name", option_name, pcmk__str_casei)) {
options.command = 'n';
} else if (pcmk__str_eq("-p", option_name, pcmk__str_casei) || pcmk__str_eq("--partition", option_name, pcmk__str_casei)) {
options.command = 'p';
} else if (pcmk__str_eq("-q", option_name, pcmk__str_casei) || pcmk__str_eq("--quorum", option_name, pcmk__str_casei)) {
options.command = 'q';
} else {
g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "Unknown param passed to command_cb: %s", option_name);
return FALSE;
}
return TRUE;
}
gboolean
name_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
options.command = 'N';
pcmk__scan_min_int(optarg, &(options.nodeid), 0);
return TRUE;
}
gboolean
remove_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
if (optarg == NULL) {
g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_INVALID_PARAM, "-R option requires an argument");
return FALSE;
}
options.command = 'R';
options.dangerous_cmd = TRUE;
pcmk__str_update(&options.target_uname, optarg);
return TRUE;
}
PCMK__OUTPUT_ARGS("node-id", "uint32_t")
static int
node_id_default(pcmk__output_t *out, va_list args) {
uint32_t node_id = va_arg(args, uint32_t);
out->info(out, "%" PRIu32, node_id);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("node-id", "uint32_t")
static int
node_id_xml(pcmk__output_t *out, va_list args) {
uint32_t node_id = va_arg(args, uint32_t);
char *id_s = crm_strdup_printf("%" PRIu32, node_id);
pcmk__output_create_xml_node(out, PCMK_XE_NODE_INFO,
PCMK_XA_NODEID, id_s,
NULL);
free(id_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("simple-node-list", "GList *")
static int
simple_node_list_default(pcmk__output_t *out, va_list args)
{
GList *nodes = va_arg(args, GList *);
for (GList *node_iter = nodes; node_iter != NULL; node_iter = node_iter->next) {
pcmk_controld_api_node_t *node = node_iter->data;
out->info(out, "%" PRIu32 " %s %s", node->id, pcmk__s(node->uname, ""),
pcmk__s(node->state, ""));
}
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("simple-node-list", "GList *")
static int
simple_node_list_xml(pcmk__output_t *out, va_list args)
{
GList *nodes = va_arg(args, GList *);
out->begin_list(out, NULL, NULL, PCMK_XE_NODES);
for (GList *node_iter = nodes; node_iter != NULL; node_iter = node_iter->next) {
pcmk_controld_api_node_t *node = node_iter->data;
char *id_s = crm_strdup_printf("%" PRIu32, node->id);
pcmk__output_create_xml_node(out, PCMK_XE_NODE,
PCMK_XA_ID, id_s,
PCMK_XA_NAME, node->uname,
PCMK_XA_STATE, node->state,
NULL);
free(id_s);
}
out->end_list(out);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("node-name", "uint32_t", "const char *")
static int
node_name_default(pcmk__output_t *out, va_list args) {
uint32_t node_id G_GNUC_UNUSED = va_arg(args, uint32_t);
const char *node_name = va_arg(args, const char *);
out->info(out, "%s", node_name);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("node-name", "uint32_t", "const char *")
static int
node_name_xml(pcmk__output_t *out, va_list args) {
uint32_t node_id = va_arg(args, uint32_t);
const char *node_name = va_arg(args, const char *);
char *id_s = crm_strdup_printf("%" PRIu32, node_id);
pcmk__output_create_xml_node(out, PCMK_XE_NODE_INFO,
PCMK_XA_NODEID, id_s,
PCMK_XA_UNAME, node_name,
NULL);
free(id_s);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("partition-list", "GList *")
static int
partition_list_default(pcmk__output_t *out, va_list args)
{
GList *nodes = va_arg(args, GList *);
GString *buffer = NULL;
for (GList *node_iter = nodes; node_iter != NULL; node_iter = node_iter->next) {
pcmk_controld_api_node_t *node = node_iter->data;
if (pcmk__str_eq(node->state, "member", pcmk__str_none)) {
pcmk__add_separated_word(&buffer, 128, pcmk__s(node->uname, ""), " ");
}
}
if (buffer != NULL) {
out->info(out, "%s", buffer->str);
g_string_free(buffer, TRUE);
return pcmk_rc_ok;
}
return pcmk_rc_no_output;
}
PCMK__OUTPUT_ARGS("partition-list", "GList *")
static int
partition_list_xml(pcmk__output_t *out, va_list args)
{
GList *nodes = va_arg(args, GList *);
out->begin_list(out, NULL, NULL, PCMK_XE_NODES);
for (GList *node_iter = nodes; node_iter != NULL; node_iter = node_iter->next) {
pcmk_controld_api_node_t *node = node_iter->data;
if (pcmk__str_eq(node->state, "member", pcmk__str_none)) {
char *id_s = crm_strdup_printf("%" PRIu32, node->id);
pcmk__output_create_xml_node(out, PCMK_XE_NODE,
PCMK_XA_ID, id_s,
PCMK_XA_NAME, node->uname,
PCMK_XA_STATE, node->state,
NULL);
free(id_s);
}
}
out->end_list(out);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("quorum", "bool")
static int
quorum_default(pcmk__output_t *out, va_list args) {
bool have_quorum = va_arg(args, int);
out->info(out, "%d", have_quorum);
return pcmk_rc_ok;
}
PCMK__OUTPUT_ARGS("quorum", "bool")
static int
quorum_xml(pcmk__output_t *out, va_list args) {
bool have_quorum = va_arg(args, int);
pcmk__output_create_xml_node(out, PCMK_XE_CLUSTER_INFO,
PCMK_XA_QUORUM, pcmk__btoa(have_quorum),
NULL);
return pcmk_rc_ok;
}
static pcmk__message_entry_t fmt_functions[] = {
{ "node-id", "default", node_id_default },
{ "node-id", "xml", node_id_xml },
{ "node-name", "default", node_name_default },
{ "node-name", "xml", node_name_xml },
{ "partition-list", "default", partition_list_default },
{ "partition-list", "xml", partition_list_xml },
{ "quorum", "default", quorum_default },
{ "quorum", "xml", quorum_xml },
{ "simple-node-list", "default", simple_node_list_default },
{ "simple-node-list", "xml", simple_node_list_xml },
{ NULL, NULL, NULL }
};
static gint
sort_node(gconstpointer a, gconstpointer b)
{
const pcmk_controld_api_node_t *node_a = a;
const pcmk_controld_api_node_t *node_b = b;
return pcmk__numeric_strcasecmp((node_a->uname? node_a->uname : ""),
(node_b->uname? node_b->uname : ""));
}
static void
controller_event_cb(pcmk_ipc_api_t *controld_api,
enum pcmk_ipc_event event_type, crm_exit_t status,
void *event_data, void *user_data)
{
pcmk_controld_api_reply_t *reply = event_data;
switch (event_type) {
case pcmk_ipc_event_disconnect:
if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Lost connection to controller");
}
goto done;
break;
case pcmk_ipc_event_reply:
break;
default:
return;
}
if (status != CRM_EX_OK) {
exit_code = status;
g_set_error(&error, PCMK__EXITC_ERROR, status,
"Bad reply from controller: %s",
crm_exit_str(status));
goto done;
}
if (reply->reply_type != pcmk_controld_reply_nodes) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_INDETERMINATE,
"Unknown reply type %d from controller",
reply->reply_type);
goto done;
}
reply->data.nodes = g_list_sort(reply->data.nodes, sort_node);
if (options.command == 'p') {
out->message(out, "partition-list", reply->data.nodes);
} else if (options.command == 'l') {
out->message(out, "simple-node-list", reply->data.nodes);
}
// Success
exit_code = CRM_EX_OK;
done:
pcmk_disconnect_ipc(controld_api);
pcmk_quit_main_loop(mainloop, 10);
}
static void
run_controller_mainloop(void)
{
pcmk_ipc_api_t *controld_api = NULL;
int rc;
// Set disconnect exit code to handle unexpected disconnects
exit_code = CRM_EX_DISCONNECT;
// Create controller IPC object
rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc,
"Could not connect to controller: %s",
pcmk_rc_str(rc));
return;
}
pcmk_register_ipc_callback(controld_api, controller_event_cb, NULL);
// Connect to controller
rc = pcmk__connect_ipc(controld_api, pcmk_ipc_dispatch_main, 5);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not connect to %s: %s",
pcmk_ipc_name(controld_api, true), pcmk_rc_str(rc));
return;
}
rc = pcmk_controld_api_list_nodes(controld_api);
if (rc != pcmk_rc_ok) {
pcmk_disconnect_ipc(controld_api);
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not ping controller: %s", pcmk_rc_str(rc));
return;
}
// Run main loop to get controller reply via controller_event_cb()
mainloop = g_main_loop_new(NULL, FALSE);
g_main_loop_run(mainloop);
g_main_loop_unref(mainloop);
mainloop = NULL;
pcmk_free_ipc_api(controld_api);
}
static void
print_node_id(void)
{
uint32_t nodeid = 0;
int rc = pcmk__query_node_info(out, &nodeid, NULL, NULL, NULL, NULL, NULL,
false, 0);
if (rc != pcmk_rc_ok) {
/* pcmk__query_node_info already sets an error message on the output object,
* so there's no need to call g_set_error here. That would just create a
* duplicate error message in the output.
*/
exit_code = pcmk_rc2exitc(rc);
return;
}
rc = out->message(out, "node-id", nodeid);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc, "Could not print node ID: %s",
pcmk_rc_str(rc));
}
exit_code = pcmk_rc2exitc(rc);
}
static void
print_node_name(uint32_t nodeid)
{
int rc = pcmk_rc_ok;
char *node_name = NULL;
if (nodeid == 0) {
// Check environment first (i.e. when called by resource agent)
const char *name = getenv("OCF_RESKEY_" CRM_META "_"
PCMK__META_ON_NODE);
if (name != NULL) {
rc = out->message(out, "node-name", 0UL, name);
goto done;
}
}
// Otherwise ask the controller
/* pcmk__query_node_name already sets an error message on the output object,
* so there's no need to call g_set_error here. That would just create a
* duplicate error message in the output.
*/
rc = pcmk__query_node_name(out, nodeid, &node_name, 0);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
return;
}
rc = out->message(out, "node-name", 0UL, node_name);
done:
if (node_name != NULL) {
free(node_name);
}
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc, "Could not print node name: %s",
pcmk_rc_str(rc));
}
exit_code = pcmk_rc2exitc(rc);
}
static void
print_quorum(void)
{
bool quorum;
int rc = pcmk__query_node_info(out, NULL, NULL, NULL, NULL, &quorum, NULL,
false, 0);
if (rc != pcmk_rc_ok) {
/* pcmk__query_node_info already sets an error message on the output object,
* so there's no need to call g_set_error here. That would just create a
* duplicate error message in the output.
*/
exit_code = pcmk_rc2exitc(rc);
return;
}
rc = out->message(out, "quorum", quorum);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc, "Could not print quorum status: %s",
pcmk_rc_str(rc));
}
exit_code = pcmk_rc2exitc(rc);
}
/*!
* \internal
* \brief Extend a transaction by removing a node from a CIB section
*
* \param[in,out] cib Active CIB connection
* \param[in] element CIB element containing node name and/or ID
* \param[in] section CIB section that \p element is in
* \param[in] node_name Name of node to purge (NULL to leave unspecified)
* \param[in] node_id Node ID of node to purge (0 to leave unspecified)
*
* \note At least one of node_name and node_id must be specified.
* \return Standard Pacemaker return code
*/
static int
remove_from_section(cib_t *cib, const char *element, const char *section,
const char *node_name, long node_id)
{
int rc = pcmk_rc_ok;
xmlNode *xml = pcmk__xe_create(NULL, element);
crm_xml_add(xml, PCMK_XA_UNAME, node_name);
if (node_id > 0) {
crm_xml_add_ll(xml, PCMK_XA_ID, node_id);
}
rc = cib->cmds->remove(cib, section, xml, cib_transaction);
pcmk__xml_free(xml);
return (rc >= 0)? pcmk_rc_ok : pcmk_legacy2rc(rc);
}
/*!
* \internal
* \brief Purge a node from CIB
*
* \param[in] node_name Name of node to purge (or NULL to leave unspecified)
* \param[in] node_id Node ID of node to purge (or 0 to leave unspecified)
*
* \note At least one of node_name and node_id must be specified.
* \return Standard Pacemaker return code
*/
static int
purge_node_from_cib(const char *node_name, long node_id)
{
int rc = pcmk_rc_ok;
int commit_rc = pcmk_rc_ok;
cib_t *cib = NULL;
// Connect to CIB and start a transaction
- cib = cib_new();
- if (cib == NULL) {
- return ENOTCONN;
- }
- rc = cib__signon_attempts(cib, cib_command, 5);
- if (rc == pcmk_ok) {
+ rc = cib__create_signon(&cib);
+ if (rc == pcmk_rc_ok) {
rc = cib->cmds->init_transaction(cib);
- }
- if (rc != pcmk_ok) {
rc = pcmk_legacy2rc(rc);
+ }
+ if (rc != pcmk_rc_ok) {
cib__clean_up_connection(&cib);
return rc;
}
// Remove from configuration and status
rc = remove_from_section(cib, PCMK_XE_NODE, PCMK_XE_NODES, node_name,
node_id);
if (rc == pcmk_rc_ok) {
rc = remove_from_section(cib, PCMK__XE_NODE_STATE, PCMK_XE_STATUS,
node_name, node_id);
}
// Commit the transaction
commit_rc = cib->cmds->end_transaction(cib, (rc == pcmk_rc_ok),
cib_sync_call);
cib__clean_up_connection(&cib);
if ((rc == pcmk_rc_ok) && (commit_rc == pcmk_ok)) {
crm_debug("Purged node %s (%ld) from CIB",
pcmk__s(node_name, "by ID"), node_id);
}
return rc;
}
/*!
* \internal
* \brief Purge a node from a single server's peer cache
*
* \param[in] server IPC server to send request to
* \param[in] node_name Name of node to purge (or NULL to leave unspecified)
* \param[in] node_id Node ID of node to purge (or 0 to leave unspecified)
*
* \note At least one of node_name and node_id must be specified.
* \return Standard Pacemaker return code
*/
static int
purge_node_from(enum pcmk_ipc_server server, const char *node_name,
long node_id)
{
pcmk_ipc_api_t *api = NULL;
int rc;
rc = pcmk_new_ipc_api(&api, server);
if (rc != pcmk_rc_ok) {
goto done;
}
rc = pcmk__connect_ipc(api, pcmk_ipc_dispatch_sync, 5);
if (rc != pcmk_rc_ok) {
goto done;
}
rc = pcmk_ipc_purge_node(api, node_name, node_id);
done:
if (rc != pcmk_rc_ok) { // Debug message already logged on success
g_set_error(&error, PCMK__RC_ERROR, rc,
"Could not purge node %s from %s: %s",
pcmk__s(node_name, "by ID"), pcmk_ipc_name(api, true),
pcmk_rc_str(rc));
}
pcmk_free_ipc_api(api);
return rc;
}
/*!
* \internal
* \brief Purge a node from the fencer's peer cache
*
* \param[in] node_name Name of node to purge (or NULL to leave unspecified)
* \param[in] node_id Node ID of node to purge (or 0 to leave unspecified)
*
* \note At least one of node_name and node_id must be specified.
* \return Standard Pacemaker return code
*/
static int
purge_node_from_fencer(const char *node_name, long node_id)
{
int rc = pcmk_rc_ok;
crm_ipc_t *conn = NULL;
xmlNode *cmd = NULL;
conn = crm_ipc_new("stonith-ng", 0);
if (conn == NULL) {
rc = ENOTCONN;
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not connect to fencer to purge node %s",
pcmk__s(node_name, "by ID"));
return rc;
}
rc = pcmk__connect_generic_ipc(conn);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not connect to fencer to purge node %s: %s",
pcmk__s(node_name, "by ID"), pcmk_rc_str(rc));
crm_ipc_destroy(conn);
return rc;
}
cmd = pcmk__new_request(pcmk_ipc_fenced, crm_system_name, NULL,
PCMK__VALUE_STONITH_NG, CRM_OP_RM_NODE_CACHE, NULL);
if (node_id > 0) {
crm_xml_add_ll(cmd, PCMK_XA_ID, node_id);
}
crm_xml_add(cmd, PCMK_XA_UNAME, node_name);
rc = crm_ipc_send(conn, cmd, 0, 0, NULL);
if (rc >= 0) {
rc = pcmk_rc_ok;
crm_debug("Purged node %s (%ld) from fencer",
pcmk__s(node_name, "by ID"), node_id);
} else {
rc = pcmk_legacy2rc(rc);
fprintf(stderr, "Could not purge node %s from fencer: %s\n",
pcmk__s(node_name, "by ID"), pcmk_rc_str(rc));
}
pcmk__xml_free(cmd);
crm_ipc_close(conn);
crm_ipc_destroy(conn);
return rc;
}
static void
remove_node(const char *target_uname)
{
int rc = pcmk_rc_ok;
long nodeid = 0;
const char *node_name = NULL;
char *endptr = NULL;
const enum pcmk_ipc_server servers[] = {
pcmk_ipc_controld,
pcmk_ipc_attrd,
};
// Check whether node was specified by name or numeric ID
errno = 0;
nodeid = strtol(target_uname, &endptr, 10);
if ((errno != 0) || (endptr == target_uname) || (*endptr != '\0')
|| (nodeid <= 0)) {
// It's not a positive integer, so assume it's a node name
nodeid = 0;
node_name = target_uname;
}
for (int i = 0; i < PCMK__NELEM(servers); ++i) {
rc = purge_node_from(servers[i], node_name, nodeid);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
return;
}
}
// The fencer hasn't been converted to pcmk_ipc_api_t yet
rc = purge_node_from_fencer(node_name, nodeid);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
return;
}
// Lastly, purge the node from the CIB itself
rc = purge_node_from_cib(node_name, nodeid);
exit_code = pcmk_rc2exitc(rc);
}
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
GOptionContext *context = NULL;
GOptionEntry extra_prog_entries[] = {
{ "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet),
"Be less descriptive in output.",
NULL },
{ NULL }
};
context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
/* Add the -q option, which cannot be part of the globally supported options
* because some tools use that flag for something else.
*/
pcmk__add_main_args(context, extra_prog_entries);
pcmk__add_arg_group(context, "commands", "Commands:",
"Show command help", command_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
int rc = pcmk_rc_ok;
GOptionGroup *output_group = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "NR");
GOptionContext *context = build_arg_context(args, &output_group);
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
pcmk__cli_init_logging("crm_node", args->verbosity);
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error creating output format %s: %s", args->output_ty,
pcmk_rc_str(rc));
goto done;
}
if (args->version) {
out->version(out);
goto done;
}
if (options.command == 0) {
char *help = g_option_context_get_help(context, TRUE, NULL);
out->err(out, "%s", help);
g_free(help);
exit_code = CRM_EX_USAGE;
goto done;
}
if (options.dangerous_cmd && options.force_flag == FALSE) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"The supplied command is considered dangerous."
" To prevent accidental destruction of the cluster,"
" the --force flag is required in order to proceed.");
goto done;
}
pcmk__register_lib_messages(out);
pcmk__register_messages(out, fmt_functions);
switch (options.command) {
case 'i':
print_node_id();
break;
case 'n':
print_node_name(0);
break;
case 'q':
print_quorum();
break;
case 'N':
print_node_name(options.nodeid);
break;
case 'R':
remove_node(options.target_uname);
break;
case 'l':
case 'p':
run_controller_mainloop();
break;
default:
break;
}
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
pcmk__output_and_clear_error(&error, out);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
pcmk__unregister_formats();
return crm_exit(exit_code);
}
diff --git a/tools/crm_resource.c b/tools/crm_resource.c
index ba137ee81e..23af05ad2e 100644
--- a/tools/crm_resource.c
+++ b/tools/crm_resource.c
@@ -1,2406 +1,2398 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include // stonith__agent_exists()
#include
#include
#include // bool, true, false
#include // uint32_t
#include
#include
#include
#include
#include
#include
#include
#include
#include // xmlXPathObject, etc.
#include
#include
#include // PCMK_RESOURCE_CLASS_*
#include
#include
#define SUMMARY "crm_resource - perform tasks related to Pacemaker cluster resources"
enum rsc_command {
cmd_ban,
cmd_cleanup,
cmd_clear,
cmd_colocations,
cmd_cts,
cmd_delete,
cmd_delete_param,
cmd_digests,
cmd_execute_agent,
cmd_fail,
cmd_get_param,
cmd_list_active_ops,
cmd_list_agents,
cmd_list_all_ops,
cmd_list_alternatives,
cmd_list_instances,
cmd_list_options,
cmd_list_providers,
cmd_list_resources,
cmd_list_standards,
cmd_locate,
cmd_metadata,
cmd_move,
cmd_query_xml,
cmd_query_xml_raw,
cmd_refresh,
cmd_restart,
cmd_set_param,
cmd_wait,
cmd_why,
// Update this when adding new commands
cmd_max = cmd_why,
};
/*!
* \internal
* \brief Handler function for a crm_resource command
*/
typedef crm_exit_t (*crm_resource_fn_t)(pcmk_resource_t *, pcmk_node_t *,
cib_t *, pcmk_scheduler_t *,
pcmk_ipc_api_t *, xmlNode *);
/*!
* \internal
* \brief Flags to define attributes of a given command
*
* These attributes may include required command-line options, how to look up a
* resource in the scheduler data, whether the command supports clone instances,
* etc.
*/
enum crm_rsc_flags {
//! Use \c pcmk_rsc_match_anon_basename when looking up a resource
crm_rsc_find_match_anon_basename = (UINT32_C(1) << 0),
//! Use \c pcmk_rsc_match_basename when looking up a resource
crm_rsc_find_match_basename = (UINT32_C(1) << 1),
//! Use \c pcmk_rsc_match_history when looking up a resource
crm_rsc_find_match_history = (UINT32_C(1) << 2),
//! Fail if \c --resource refers to a particular clone instance
crm_rsc_rejects_clone_instance = (UINT32_C(1) << 3),
//! Require CIB connection unless resource is specified by agent
crm_rsc_requires_cib = (UINT32_C(1) << 4),
//! Require controller connection
crm_rsc_requires_controller = (UINT32_C(1) << 5),
//! Require \c --node argument
crm_rsc_requires_node = (UINT32_C(1) << 6),
//! Require \c --resource argument
crm_rsc_requires_resource = (UINT32_C(1) << 7),
//! Require scheduler data unless resource is specified by agent
crm_rsc_requires_scheduler = (UINT32_C(1) << 8),
};
/*!
* \internal
* \brief Handler function and flags for a given command
*/
typedef struct {
crm_resource_fn_t fn; //!< Command handler function
uint32_t flags; //!< Group of enum crm_rsc_flags
} crm_resource_cmd_info_t;
struct {
enum rsc_command rsc_cmd; // crm_resource command to perform
// Command-line option values
gchar *rsc_id; // Value of --resource
gchar *rsc_type; // Value of --resource-type
gboolean all; // --all was given
gboolean force; // --force was given
gboolean clear_expired; // --expired was given
gboolean recursive; // --recursive was given
gboolean promoted_role_only; // --promoted was given
gchar *host_uname; // Value of --node
gchar *interval_spec; // Value of --interval
gchar *move_lifetime; // Value of --lifetime
gchar *operation; // Value of --operation
enum pcmk__opt_flags opt_list; // Parsed from --list-options
const char *attr_set_type; // Instance, meta, utilization, or element attribute
gchar *prop_id; // --nvpair (attribute XML ID)
char *prop_name; // Attribute name
gchar *prop_set; // --set-name (attribute block XML ID)
gchar *prop_value; // --parameter-value (attribute value)
guint timeout_ms; // Parsed from --timeout value
char *agent_spec; // Standard and/or provider and/or agent
int check_level; // Optional value of --validate or --force-check
// Resource configuration specified via command-line arguments
gchar *agent; // Value of --agent
gchar *class; // Value of --class
gchar *provider; // Value of --provider
GHashTable *cmdline_params; // Resource parameters specified
// Positional command-line arguments
gchar **remainder; // Positional arguments as given
GHashTable *override_params; // Resource parameter values that override config
} options = {
.attr_set_type = PCMK_XE_INSTANCE_ATTRIBUTES,
.check_level = -1,
.rsc_cmd = cmd_list_resources, // List all resources if no command given
};
static crm_exit_t exit_code = CRM_EX_OK;
static pcmk__output_t *out = NULL;
static pcmk__common_args_t *args = NULL;
// Things that should be cleaned up on exit
static GError *error = NULL;
static GMainLoop *mainloop = NULL;
#define MESSAGE_TIMEOUT_S 60
#define INDENT " "
static pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
static void
quit_main_loop(crm_exit_t ec)
{
exit_code = ec;
if (mainloop != NULL) {
GMainLoop *mloop = mainloop;
mainloop = NULL; // Don't re-enter this block
pcmk_quit_main_loop(mloop, 10);
g_main_loop_unref(mloop);
}
}
static gboolean
resource_ipc_timeout(gpointer data)
{
// Start with newline because "Waiting for ..." message doesn't have one
if (error != NULL) {
g_clear_error(&error);
}
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_TIMEOUT,
_("Aborting because no messages received in %d seconds"), MESSAGE_TIMEOUT_S);
quit_main_loop(CRM_EX_TIMEOUT);
return FALSE;
}
static void
controller_event_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type,
crm_exit_t status, void *event_data, void *user_data)
{
crm_exit_t *ec = user_data;
pcmk__assert(ec != NULL);
switch (event_type) {
case pcmk_ipc_event_disconnect:
if (exit_code == CRM_EX_DISCONNECT) { // Unexpected
crm_info("Connection to controller was terminated");
}
*ec = exit_code;
quit_main_loop(*ec);
break;
case pcmk_ipc_event_reply:
if (status != CRM_EX_OK) {
out->err(out, "Error: bad reply from controller: %s",
crm_exit_str(status));
pcmk_disconnect_ipc(api);
*ec = status;
quit_main_loop(*ec);
} else {
if ((pcmk_controld_api_replies_expected(api) == 0)
&& (mainloop != NULL)
&& g_main_loop_is_running(mainloop)) {
out->info(out, "... got reply (done)");
crm_debug("Got all the replies we expected");
pcmk_disconnect_ipc(api);
*ec = CRM_EX_OK;
quit_main_loop(*ec);
} else {
out->info(out, "... got reply");
}
}
break;
default:
break;
}
}
static void
start_mainloop(pcmk_ipc_api_t *capi)
{
// @TODO See if we can avoid setting exit_code as a global variable
unsigned int count = pcmk_controld_api_replies_expected(capi);
if (count > 0) {
out->info(out, "Waiting for %u %s from the controller",
count, pcmk__plural_alt(count, "reply", "replies"));
exit_code = CRM_EX_DISCONNECT; // For unexpected disconnects
mainloop = g_main_loop_new(NULL, FALSE);
pcmk__create_timer(MESSAGE_TIMEOUT_S * 1000, resource_ipc_timeout, NULL);
g_main_loop_run(mainloop);
}
}
static GList *
build_constraint_list(xmlNode *root)
{
GList *retval = NULL;
xmlNode *cib_constraints = NULL;
xmlXPathObject *xpathObj = NULL;
int ndx = 0;
int num_results = 0;
cib_constraints = pcmk_find_cib_element(root, PCMK_XE_CONSTRAINTS);
xpathObj = pcmk__xpath_search(cib_constraints->doc,
"//" PCMK_XE_RSC_LOCATION);
num_results = pcmk__xpath_num_results(xpathObj);
for (ndx = 0; ndx < num_results; ndx++) {
xmlNode *match = pcmk__xpath_result(xpathObj, ndx);
if (match != NULL) {
retval = g_list_insert_sorted(retval, (gpointer) pcmk__xe_id(match),
(GCompareFunc) g_strcmp0);
}
}
xmlXPathFreeObject(xpathObj);
return retval;
}
static gboolean
validate_opt_list(const gchar *optarg)
{
if (pcmk__str_eq(optarg, PCMK_VALUE_FENCING, pcmk__str_none)) {
options.opt_list = pcmk__opt_fencing;
} else if (pcmk__str_eq(optarg, PCMK__VALUE_PRIMITIVE, pcmk__str_none)) {
options.opt_list = pcmk__opt_primitive;
} else {
return FALSE;
}
return TRUE;
}
// GOptionArgFunc callback functions
static gboolean
attr_set_type_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error) {
if (pcmk__str_any_of(option_name, "-m", "--meta", NULL)) {
options.attr_set_type = PCMK_XE_META_ATTRIBUTES;
} else if (pcmk__str_any_of(option_name, "-z", "--utilization", NULL)) {
options.attr_set_type = PCMK_XE_UTILIZATION;
} else if (pcmk__str_eq(option_name, "--element", pcmk__str_none)) {
options.attr_set_type = ATTR_SET_ELEMENT;
}
return TRUE;
}
/*!
* \internal
* \brief Process options that set the command
*
* Nothing else should set \c options.rsc_cmd.
*
* \param[in] option_name Name of the option being parsed
* \param[in] optarg Value to be parsed
* \param[in] data Ignored
* \param[out] error Where to store recoverable error, if any
*
* \return \c TRUE if the option was successfully parsed, or \c FALSE if an
* error occurred, in which case \p *error is set
*/
static gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
// Sorted by enum rsc_command name
if (pcmk__str_any_of(option_name, "-B", "--ban", NULL)) {
options.rsc_cmd = cmd_ban;
} else if (pcmk__str_any_of(option_name, "-C", "--cleanup", NULL)) {
options.rsc_cmd = cmd_cleanup;
} else if (pcmk__str_any_of(option_name, "-U", "--clear", NULL)) {
options.rsc_cmd = cmd_clear;
} else if (pcmk__str_any_of(option_name, "-a", "--constraints", NULL)) {
options.rsc_cmd = cmd_colocations;
} else if (pcmk__str_any_of(option_name, "-A", "--stack", NULL)) {
options.rsc_cmd = cmd_colocations;
options.recursive = TRUE;
} else if (pcmk__str_any_of(option_name, "-c", "--list-cts", NULL)) {
options.rsc_cmd = cmd_cts;
} else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
options.rsc_cmd = cmd_delete;
} else if (pcmk__str_any_of(option_name, "-d", "--delete-parameter",
NULL)) {
options.rsc_cmd = cmd_delete_param;
pcmk__str_update(&options.prop_name, optarg);
} else if (pcmk__str_eq(option_name, "--digests", pcmk__str_none)) {
options.rsc_cmd = cmd_digests;
if (options.override_params == NULL) {
options.override_params = pcmk__strkey_table(g_free, g_free);
}
} else if (pcmk__str_any_of(option_name,
"--force-demote", "--force-promote",
"--force-start", "--force-stop",
"--force-check", "--validate", NULL)) {
options.rsc_cmd = cmd_execute_agent;
g_free(options.operation);
options.operation = g_strdup(option_name + 2); // skip "--"
if (options.override_params == NULL) {
options.override_params = pcmk__strkey_table(g_free, g_free);
}
if (optarg != NULL) {
if (pcmk__scan_min_int(optarg, &options.check_level,
0) != pcmk_rc_ok) {
g_set_error(error, G_OPTION_ERROR, CRM_EX_INVALID_PARAM,
_("Invalid check level setting: %s"), optarg);
return FALSE;
}
}
} else if (pcmk__str_any_of(option_name, "-F", "--fail", NULL)) {
options.rsc_cmd = cmd_fail;
} else if (pcmk__str_any_of(option_name, "-g", "--get-parameter", NULL)) {
options.rsc_cmd = cmd_get_param;
pcmk__str_update(&options.prop_name, optarg);
} else if (pcmk__str_any_of(option_name, "-O", "--list-operations", NULL)) {
options.rsc_cmd = cmd_list_active_ops;
} else if (pcmk__str_eq(option_name, "--list-agents", pcmk__str_none)) {
options.rsc_cmd = cmd_list_agents;
pcmk__str_update(&options.agent_spec, optarg);
} else if (pcmk__str_any_of(option_name, "-o", "--list-all-operations",
NULL)) {
options.rsc_cmd = cmd_list_all_ops;
} else if (pcmk__str_eq(option_name, "--list-ocf-alternatives",
pcmk__str_none)) {
options.rsc_cmd = cmd_list_alternatives;
pcmk__str_update(&options.agent_spec, optarg);
} else if (pcmk__str_eq(option_name, "--list-options", pcmk__str_none)) {
options.rsc_cmd = cmd_list_options;
return validate_opt_list(optarg);
} else if (pcmk__str_any_of(option_name, "-l", "--list-raw", NULL)) {
options.rsc_cmd = cmd_list_instances;
} else if (pcmk__str_eq(option_name, "--list-ocf-providers",
pcmk__str_none)) {
options.rsc_cmd = cmd_list_providers;
pcmk__str_update(&options.agent_spec, optarg);
} else if (pcmk__str_any_of(option_name, "-L", "--list", NULL)) {
options.rsc_cmd = cmd_list_resources;
} else if (pcmk__str_eq(option_name, "--list-standards", pcmk__str_none)) {
options.rsc_cmd = cmd_list_standards;
} else if (pcmk__str_any_of(option_name, "-W", "--locate", NULL)) {
options.rsc_cmd = cmd_locate;
} else if (pcmk__str_eq(option_name, "--show-metadata", pcmk__str_none)) {
options.rsc_cmd = cmd_metadata;
pcmk__str_update(&options.agent_spec, optarg);
} else if (pcmk__str_any_of(option_name, "-M", "--move", NULL)) {
options.rsc_cmd = cmd_move;
} else if (pcmk__str_any_of(option_name, "-q", "--query-xml", NULL)) {
options.rsc_cmd = cmd_query_xml;
} else if (pcmk__str_any_of(option_name, "-w", "--query-xml-raw", NULL)) {
options.rsc_cmd = cmd_query_xml_raw;
} else if (pcmk__str_any_of(option_name, "-R", "--refresh", NULL)) {
options.rsc_cmd = cmd_refresh;
} else if (pcmk__str_eq(option_name, "--restart", pcmk__str_none)) {
options.rsc_cmd = cmd_restart;
} else if (pcmk__str_any_of(option_name, "-p", "--set-parameter", NULL)) {
options.rsc_cmd = cmd_set_param;
pcmk__str_update(&options.prop_name, optarg);
} else if (pcmk__str_eq(option_name, "--wait", pcmk__str_none)) {
options.rsc_cmd = cmd_wait;
} else if (pcmk__str_any_of(option_name, "-Y", "--why", NULL)) {
options.rsc_cmd = cmd_why;
}
return TRUE;
}
static gboolean
option_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
gchar *name = NULL;
gchar *value = NULL;
if (pcmk__scan_nvpair(optarg, &name, &value) != pcmk_rc_ok) {
return FALSE;
}
/* services__create_resource_action() ultimately takes ownership of
* options.cmdline_params. It's not worth trying to ensure that the entire
* call path uses (gchar *) strings and g_free(). So create the table for
* (char *) strings, and duplicate the (gchar *) strings when inserting.
*/
if (options.cmdline_params == NULL) {
options.cmdline_params = pcmk__strkey_table(free, free);
}
pcmk__insert_dup(options.cmdline_params, name, value);
g_free(name);
g_free(value);
return TRUE;
}
static gboolean
timeout_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
long long timeout_ms = crm_get_msec(optarg);
if (timeout_ms < 0) {
return FALSE;
}
options.timeout_ms = (guint) QB_MIN(timeout_ms, UINT_MAX);
return TRUE;
}
// Command line option specification
/* short option letters still available: eEJkKXyYZ */
static GOptionEntry query_entries[] = {
{ "list", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"List all cluster resources with status",
NULL },
{ "list-raw", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"List IDs of all instantiated resources (individual members\n"
INDENT "rather than groups etc.)",
NULL },
{ "list-cts", 'c', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG,
G_OPTION_ARG_CALLBACK, command_cb,
NULL,
NULL },
{ "list-operations", 'O', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"List active resource operations, optionally filtered by\n"
INDENT "--resource and/or --node",
NULL },
{ "list-all-operations", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"List all resource operations, optionally filtered by\n"
INDENT "--resource and/or --node",
NULL },
{ "list-options", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"List all available options of the given type.\n"
INDENT "Allowed values:\n"
INDENT PCMK__VALUE_PRIMITIVE " (primitive resource meta-attributes),\n"
INDENT PCMK_VALUE_FENCING " (parameters common to all fencing resources)",
"TYPE" },
{ "list-standards", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"List supported standards",
NULL },
{ "list-ocf-providers", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"List all available OCF providers",
NULL },
{ "list-agents", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"List all agents available for the named standard and/or provider",
"STD:PROV" },
{ "list-ocf-alternatives", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"List all available providers for the named OCF agent",
"AGENT" },
{ "show-metadata", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Show the metadata for the named class:provider:agent",
"SPEC" },
{ "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Show XML configuration of resource (after any template expansion)",
NULL },
{ "query-xml-raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"Show XML configuration of resource (before any template expansion)",
NULL },
{ "get-parameter", 'g', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"Display named parameter for resource (use instance attribute\n"
INDENT "unless --element, --meta, or --utilization is specified)",
"PARAM" },
{ "locate", 'W', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Show node(s) currently running resource",
NULL },
{ "constraints", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"Display the location and colocation constraints that apply to a\n"
INDENT "resource, and if --recursive is specified, to the resources\n"
INDENT "directly or indirectly involved in those colocations.\n"
INDENT "If the named resource is part of a group, or a clone or\n"
INDENT "bundle instance, constraints for the collective resource\n"
INDENT "will be shown unless --force is given.",
NULL },
{ "stack", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Equivalent to --constraints --recursive",
NULL },
{ "why", 'Y', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Show why resources are not running, optionally filtered by\n"
INDENT "--resource and/or --node",
NULL },
{ NULL }
};
static GOptionEntry command_entries[] = {
{ "validate", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"Validate resource configuration by calling agent's validate-all\n"
INDENT "action. The configuration may be specified either by giving an\n"
INDENT "existing resource name with -r, or by specifying --class,\n"
INDENT "--agent, and --provider arguments, along with any number of\n"
INDENT "--option arguments. An optional LEVEL argument can be given\n"
INDENT "to control the level of checking performed.",
"LEVEL" },
{ "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"If resource has any past failures, clear its history and fail\n"
INDENT "count. Optionally filtered by --resource, --node, --operation\n"
INDENT "and --interval (otherwise all). --operation and --interval\n"
INDENT "apply to fail counts, but entire history is always clear, to\n"
INDENT "allow current state to be rechecked. If the named resource is\n"
INDENT "part of a group, or one numbered instance of a clone or bundled\n"
INDENT "resource, the clean-up applies to the whole collective resource\n"
INDENT "unless --force is given.",
NULL },
{ "refresh", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Delete resource's history (including failures) so its current state\n"
INDENT "is rechecked. Optionally filtered by --resource and --node\n"
INDENT "(otherwise all). If the named resource is part of a group, or one\n"
INDENT "numbered instance of a clone or bundled resource, the refresh\n"
INDENT "applies to the whole collective resource unless --force is given.",
NULL },
{ "set-parameter", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"Set named parameter for resource (requires -v). Use instance\n"
INDENT "attribute unless --element, --meta, or --utilization is "
"specified.",
"PARAM" },
{ "delete-parameter", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"Delete named parameter for resource. Use instance attribute\n"
INDENT "unless --element, --meta or, --utilization is specified.",
"PARAM" },
{ NULL }
};
static GOptionEntry location_entries[] = {
{ "move", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Create a constraint to move resource. If --node is specified,\n"
INDENT "the constraint will be to move to that node, otherwise it\n"
INDENT "will be to ban the current node. Unless --force is specified\n"
INDENT "this will return an error if the resource is already running\n"
INDENT "on the specified node. If --force is specified, this will\n"
INDENT "always ban the current node.\n"
INDENT "Optional: --lifetime, --promoted. NOTE: This may prevent the\n"
INDENT "resource from running on its previous location until the\n"
INDENT "implicit constraint expires or is removed with --clear.",
NULL },
{ "ban", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Create a constraint to keep resource off a node.\n"
INDENT "Optional: --node, --lifetime, --promoted.\n"
INDENT "NOTE: This will prevent the resource from running on the\n"
INDENT "affected node until the implicit constraint expires or is\n"
INDENT "removed with --clear. If --node is not specified, it defaults\n"
INDENT "to the node currently running the resource for primitives\n"
INDENT "and groups, or the promoted instance of promotable clones with\n"
INDENT PCMK_META_PROMOTED_MAX "=1 (all other situations result in an\n"
INDENT "error as there is no sane default).",
NULL },
{ "clear", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Remove all constraints created by the --ban and/or --move\n"
INDENT "commands. Requires: --resource. Optional: --node, --promoted,\n"
INDENT "--expired. If --node is not specified, all constraints created\n"
INDENT "by --ban and --move will be removed for the named resource. If\n"
INDENT "--node and --force are specified, any constraint created by\n"
INDENT "--move will be cleared, even if it is not for the specified\n"
INDENT "node. If --expired is specified, only those constraints whose\n"
INDENT "lifetimes have expired will be removed.",
NULL },
{ "expired", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.clear_expired,
"Modifies the --clear argument to remove constraints with\n"
INDENT "expired lifetimes.",
NULL },
{ "lifetime", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.move_lifetime,
"Lifespan (as ISO 8601 duration) of created constraints (with\n"
INDENT "-B, -M) see https://en.wikipedia.org/wiki/ISO_8601#Durations)",
"TIMESPEC" },
{ "promoted", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.promoted_role_only,
"Limit scope of command to promoted role (with -B, -M, -U). For\n"
INDENT "-B and -M, previously promoted instances may remain\n"
INDENT "active in the unpromoted role.",
NULL },
// Deprecated since 2.1.0
{ "master", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
&options.promoted_role_only,
"Deprecated: Use --promoted instead", NULL },
{ NULL }
};
static GOptionEntry advanced_entries[] = {
{ "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Delete a resource from the CIB. Required: -t",
NULL },
{ "fail", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Tell the cluster this resource has failed",
NULL },
{ "restart", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Tell the cluster to restart this resource and\n"
INDENT "anything that depends on it. This temporarily modifies\n"
INDENT "the CIB, and other CIB modifications should be avoided\n"
INDENT "while this is in progress. If a node is fenced because\n"
INDENT "the stop portion of the restart fails, CIB modifications\n"
INDENT "such as target-role may remain.",
NULL },
{ "wait", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Wait until the cluster settles into a stable state",
NULL },
{ "digests", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Show parameter hashes that Pacemaker uses to detect\n"
INDENT "configuration changes (only accurate if there is resource\n"
INDENT "history on the specified node). Required: --resource, --node.\n"
INDENT "Optional: any NAME=VALUE parameters will be used to override\n"
INDENT "the configuration (to see what the hash would be with those\n"
INDENT "changes).",
NULL },
{ "force-demote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"(Advanced) Bypass the cluster and demote a resource on the local\n"
INDENT "node. Unless --force is specified, this will refuse to do so if\n"
INDENT "the cluster believes the resource is a clone instance already\n"
INDENT "running on the local node.",
NULL },
{ "force-stop", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Bypass the cluster and stop a resource on the local node",
NULL },
{ "force-start", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Bypass the cluster and start a resource on the local\n"
INDENT "node. Unless --force is specified, this will refuse to do so if\n"
INDENT "the cluster believes the resource is a clone instance already\n"
INDENT "running on the local node.",
NULL },
{ "force-promote", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"(Advanced) Bypass the cluster and promote a resource on the local\n"
INDENT "node. Unless --force is specified, this will refuse to do so if\n"
INDENT "the cluster believes the resource is a clone instance already\n"
INDENT "running on the local node.",
NULL },
{ "force-check", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
command_cb,
"(Advanced) Bypass the cluster and check the state of a resource on\n"
INDENT "the local node. An optional LEVEL argument can be given\n"
INDENT "to control the level of checking performed.",
"LEVEL" },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.host_uname,
"Node name",
"NAME" },
{ "recursive", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.recursive,
"Follow colocation chains when using --set-parameter or --constraints",
NULL },
{ "resource-type", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_type,
"Resource XML element (primitive, group, etc.) (with -D)",
"ELEMENT" },
{ "parameter-value", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_value,
"Value to use with -p",
"PARAM" },
{ "meta", 'm', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
"Use resource meta-attribute instead of instance attribute\n"
INDENT "(with -p, -g, -d)",
NULL },
{ "utilization", 'z', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
"Use resource utilization attribute instead of instance attribute\n"
INDENT "(with -p, -g, -d)",
NULL },
{ "element", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attr_set_type_cb,
"Use resource element attribute instead of instance attribute\n"
INDENT "(with -p, -g, -d)",
NULL },
{ "operation", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.operation,
"Operation to clear instead of all (with -C -r)",
"OPERATION" },
{ "interval", 'I', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.interval_spec,
"Interval of operation to clear (default 0s) (with -C -r -n)",
"N" },
{ "class", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.class,
"The standard the resource agent conforms to (for example, ocf).\n"
INDENT "Use with --agent, --provider, --option, and --validate.",
"CLASS" },
{ "agent", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.agent,
"The agent to use (for example, IPaddr). Use with --class,\n"
INDENT "--provider, --option, and --validate.",
"AGENT" },
{ "provider", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.provider,
"The vendor that supplies the resource agent (for example,\n"
INDENT "heartbeat). Use with --class, --agent, --option, and --validate.",
"PROVIDER" },
{ "option", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, option_cb,
"Specify a device configuration parameter as NAME=VALUE (may be\n"
INDENT "specified multiple times). Use with --validate and without the\n"
INDENT "-r option.",
"PARAM" },
{ "set-name", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_set,
"(Advanced) XML ID of attributes element to use (with -p, -d)",
"ID" },
{ "nvpair", 'i', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.prop_id,
"(Advanced) XML ID of nvpair element to use (with -p, -d)",
"ID" },
{ "timeout", 'T', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, timeout_cb,
"(Advanced) Abort if command does not finish in this time (with\n"
INDENT "--restart, --wait, --force-*)",
"N" },
{ "all", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.all,
"List all options, including advanced and deprecated (with\n"
INDENT "--list-options)",
NULL },
{ "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
"Force the action to be performed. See help for individual commands for\n"
INDENT "additional behavior.",
NULL },
// @COMPAT Used in resource-agents prior to v4.2.0
{ "host-uname", 'H', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.host_uname,
NULL,
"HOST" },
{ NULL }
};
static int
ban_or_move(pcmk__output_t *out, pcmk_resource_t *rsc, cib_t *cib_conn,
const char *move_lifetime)
{
int rc = pcmk_rc_ok;
pcmk_node_t *current = NULL;
unsigned int nactive = 0;
CRM_CHECK(rsc != NULL, return EINVAL);
current = pe__find_active_requires(rsc, &nactive);
if (nactive == 1) {
rc = cli_resource_ban(out, options.rsc_id, current->priv->name,
move_lifetime, cib_conn,
options.promoted_role_only, PCMK_ROLE_PROMOTED);
} else if (pcmk_is_set(rsc->flags, pcmk__rsc_promotable)) {
int count = 0;
GList *iter = NULL;
current = NULL;
for (iter = rsc->priv->children; iter != NULL; iter = iter->next) {
pcmk_resource_t *child = (pcmk_resource_t *)iter->data;
enum rsc_role_e child_role = child->priv->fns->state(child, true);
if (child_role == pcmk_role_promoted) {
count++;
current = pcmk__current_node(child);
}
}
if(count == 1 && current) {
rc = cli_resource_ban(out, options.rsc_id, current->priv->name,
move_lifetime, cib_conn,
options.promoted_role_only,
PCMK_ROLE_PROMOTED);
} else {
rc = EINVAL;
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("Resource '%s' not moved: active in %d locations (promoted in %d).\n"
"To prevent '%s' from running on a specific location, "
"specify a node."
"To prevent '%s' from being promoted at a specific "
"location, specify a node and the --promoted option."),
options.rsc_id, nactive, count, options.rsc_id, options.rsc_id);
}
} else {
rc = EINVAL;
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("Resource '%s' not moved: active in %d locations.\n"
"To prevent '%s' from running on a specific location, "
"specify a node."),
options.rsc_id, nactive, options.rsc_id);
}
return rc;
}
static void
cleanup(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk_node_t *node,
pcmk_ipc_api_t *controld_api)
{
int rc = pcmk_rc_ok;
if (options.force == FALSE) {
rsc = uber_parent(rsc);
}
crm_debug("Erasing failures of %s (%s requested) on %s",
rsc->id, options.rsc_id,
((node != NULL)? pcmk__node_name(node) : "all nodes"));
rc = cli_resource_delete(controld_api, rsc, node, options.operation,
options.interval_spec, true, options.force);
if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
// Show any reasons why resource might stay stopped
cli_resource_check(out, rsc, node);
}
/* @FIXME The mainloop functions in this file set exit_code. What happens to
* exit_code if rc != pcmk_rc_ok here?
*/
if (rc == pcmk_rc_ok) {
start_mainloop(controld_api);
}
}
/*!
* \internal
* \brief Allocate a scheduler data object and initialize it from the CIB
*
* We transform the queried CIB XML to the latest schema version before using it
* to populate the scheduler data.
*
* \param[out] scheduler Where to store scheduler data
* \param[in] cib_conn CIB connection
* \param[in] out Output object for new scheduler data object
* \param[out] cib_xml_orig Where to store queried CIB XML from before any
* schema upgrades
*
* \return Standard Pacemaker return code
*
* \note \p *scheduler and \p *cib_xml_orig must be \c NULL when this function
* is called.
* \note The caller is responsible for freeing \p *scheduler using
* \c pcmk_free_scheduler.
*/
static int
initialize_scheduler_data(pcmk_scheduler_t **scheduler, cib_t *cib_conn,
pcmk__output_t *out, xmlNode **cib_xml_orig)
{
int rc = pcmk_rc_ok;
pcmk__assert((scheduler != NULL) && (*scheduler == NULL)
&& (cib_conn != NULL) && (out != NULL)
&& (cib_xml_orig != NULL) && (*cib_xml_orig == NULL));
*scheduler = pcmk_new_scheduler();
if (*scheduler == NULL) {
return ENOMEM;
}
pcmk__set_scheduler_flags(*scheduler, pcmk__sched_no_counts);
(*scheduler)->priv->out = out;
rc = update_scheduler_input(out, *scheduler, cib_conn, cib_xml_orig);
if (rc != pcmk_rc_ok) {
pcmk_free_scheduler(*scheduler);
*scheduler = NULL;
return rc;
}
cluster_status(*scheduler);
return pcmk_rc_ok;
}
static crm_exit_t
refresh(pcmk__output_t *out, const pcmk_node_t *node,
pcmk_ipc_api_t *controld_api)
{
const char *node_name = NULL;
const char *log_node_name = "all nodes";
const char *router_node = NULL;
int attr_options = pcmk__node_attr_none;
int rc = pcmk_rc_ok;
if (node != NULL) {
node_name = node->priv->name;
log_node_name = pcmk__node_name(node);
router_node = node->priv->name;
}
if (pcmk__is_pacemaker_remote_node(node)) {
const pcmk_node_t *conn_host = pcmk__current_node(node->priv->remote);
if (conn_host == NULL) {
rc = ENXIO;
g_set_error(&error, PCMK__RC_ERROR, rc,
_("No cluster connection to Pacemaker Remote node %s "
"detected"),
log_node_name);
return pcmk_rc2exitc(rc);
}
router_node = conn_host->priv->name;
pcmk__set_node_attr_flags(attr_options, pcmk__node_attr_remote);
}
if (controld_api == NULL) {
out->info(out, "Dry run: skipping clean-up of %s due to CIB_file",
log_node_name);
return CRM_EX_OK;
}
crm_debug("Re-checking the state of all resources on %s", log_node_name);
// @FIXME We shouldn't discard rc here
rc = pcmk__attrd_api_clear_failures(NULL, node_name, NULL, NULL, NULL, NULL,
attr_options);
/* @FIXME The mainloop functions in this file set exit_code. What happens to
* exit_code if pcmk_controld_api_reprobe() doesn't return pcmk_rc_ok?
*/
if (pcmk_controld_api_reprobe(controld_api, node_name,
router_node) == pcmk_rc_ok) {
start_mainloop(controld_api);
return exit_code;
}
return pcmk_rc2exitc(rc);
}
static void
refresh_resource(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk_node_t *node,
pcmk_ipc_api_t *controld_api)
{
int rc = pcmk_rc_ok;
if (options.force == FALSE) {
rsc = uber_parent(rsc);
}
crm_debug("Re-checking the state of %s (%s requested) on %s",
rsc->id, options.rsc_id,
((node != NULL)? pcmk__node_name(node) : "all nodes"));
rc = cli_resource_delete(controld_api, rsc, node, NULL, 0, false,
options.force);
if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) {
// Show any reasons why resource might stay stopped
cli_resource_check(out, rsc, node);
}
/* @FIXME The mainloop functions in this file set exit_code. What happens to
* exit_code if rc != pcmk_rc_ok here?
*/
if (rc == pcmk_rc_ok) {
start_mainloop(controld_api);
}
}
/*!
* \internal
* \brief Check whether a command-line resource configuration was given
*
* \return \c true if \c --class, \c --provider, or \c --agent was specified, or
* \c false otherwise
*/
static inline bool
has_cmdline_config(void)
{
return ((options.class != NULL) || (options.provider != NULL)
|| (options.agent != NULL));
}
static void
validate_cmdline_config(void)
{
bool is_ocf = pcmk__str_eq(options.class, PCMK_RESOURCE_CLASS_OCF,
pcmk__str_none);
// Sanity check before throwing any errors
if (!has_cmdline_config()) {
return;
}
// Cannot use both --resource and command-line resource configuration
if (options.rsc_id != NULL) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("--class, --agent, and --provider cannot be used with "
"-r/--resource"));
return;
}
/* Check whether command supports command-line resource configuration
*
* @FIXME According to the help text, these options can only be used with
* --validate. The --force-* commands are documented for resources that are
* configured in Pacemaker. So this is a bug. We have two choices:
* * Throw an error if --force-* commands are used with these options.
* * Document that --force-* commands can be used with these options.
*
* An error seems safer. If a user really wants to run a non-trivial
* resource action based on CLI parameters, they can do so by executing the
* resource agent directly. It's unsafe to do so if Pacemaker is managing
* the resource that's specified via --class, --option, etc.
*
* On the other hand, besides safety concerns, running other actions is
* exactly the same as running a validate action, and the implementation is
* already in place.
*/
if (options.rsc_cmd != cmd_execute_agent) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("--class, --agent, and --provider can only be used with "
"--validate and --force-*"));
return;
}
// Check for a valid combination of --class, --agent, and --provider
if (is_ocf) {
if ((options.provider == NULL) || (options.agent == NULL)) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("--provider and --agent are required with "
"--class=ocf"));
return;
}
} else {
if (options.provider != NULL) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("--provider is supported only with --class=ocf"));
return;
}
// Either --class or --agent was given
if (options.agent == NULL) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("--agent is required with --class"));
return;
}
if (options.class == NULL) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("--class is required with --agent"));
return;
}
}
// Check whether agent exists
if (pcmk__str_eq(options.class, PCMK_RESOURCE_CLASS_STONITH,
pcmk__str_none)) {
if (!stonith__agent_exists(options.agent)) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("%s is not a known stonith agent"), options.agent);
return;
}
} else if (!resources_agent_exists(options.class, options.provider,
options.agent)) {
if (is_ocf) {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("%s:%s:%s is not a known resource agent"),
options.class, options.provider, options.agent);
} else {
g_set_error(&error, PCMK__EXITC_ERROR, CRM_EX_USAGE,
_("%s:%s is not a known resource agent"),
options.class, options.agent);
}
return;
}
if (options.cmdline_params == NULL) {
options.cmdline_params = pcmk__strkey_table(free, free);
}
}
static crm_exit_t
handle_ban(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk_rc_ok;
if (node == NULL) {
rc = ban_or_move(out, rsc, cib_conn, options.move_lifetime);
} else {
rc = cli_resource_ban(out, options.rsc_id, node->priv->name,
options.move_lifetime, cib_conn,
options.promoted_role_only, PCMK_ROLE_PROMOTED);
}
if (rc == EINVAL) {
return CRM_EX_USAGE;
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_cleanup(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
if (rsc == NULL) {
int rc = cli_cleanup_all(controld_api, node, options.operation,
options.interval_spec, scheduler);
if (rc == pcmk_rc_ok) {
start_mainloop(controld_api);
}
} else {
cleanup(out, rsc, node, controld_api);
}
/* @FIXME Both of the blocks above are supposed to set exit_code via
* start_mainloop(). But if cli_cleanup_all() or cli_resource_delete()
* fails, we never start the mainloop. It looks as if we exit with CRM_EX_OK
* in those cases.
*/
return exit_code;
}
static crm_exit_t
handle_clear(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
const char *node_name = (node != NULL)? node->priv->name : NULL;
GList *before = NULL;
GList *after = NULL;
GList *remaining = NULL;
int rc = pcmk_rc_ok;
if (!out->is_quiet(out)) {
before = build_constraint_list(scheduler->input);
}
if (options.clear_expired) {
rc = cli_resource_clear_all_expired(scheduler->input, cib_conn,
options.rsc_id, node_name,
options.promoted_role_only);
} else if (node != NULL) {
rc = cli_resource_clear(options.rsc_id, node_name, NULL, cib_conn, true,
options.force);
} else {
rc = cli_resource_clear(options.rsc_id, NULL, scheduler->nodes,
cib_conn, true, options.force);
}
if (!out->is_quiet(out)) {
xmlNode *cib_xml = NULL;
rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml, cib_sync_call);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc,
_("Could not get modified CIB: %s"), pcmk_rc_str(rc));
g_list_free(before);
pcmk__xml_free(cib_xml);
return pcmk_rc2exitc(rc);
}
scheduler->input = cib_xml;
cluster_status(scheduler);
after = build_constraint_list(scheduler->input);
remaining = pcmk__subtract_lists(before, after, (GCompareFunc) strcmp);
for (const GList *iter = remaining; iter != NULL; iter = iter->next) {
const char *constraint = iter->data;
out->info(out, "Removing constraint: %s", constraint);
}
g_list_free(before);
g_list_free(after);
g_list_free(remaining);
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_colocations(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = out->message(out, "locations-and-colocations", rsc,
options.recursive, options.force);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_cts(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
g_list_foreach(scheduler->priv->resources, (GFunc) cli_resource_print_cts,
out);
cli_resource_print_cts_constraints(scheduler);
return CRM_EX_OK;
}
static crm_exit_t
handle_delete(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
/* rsc_id was already checked for NULL much earlier when validating command
* line arguments
*/
int rc = pcmk_rc_ok;
if (options.rsc_type == NULL) {
crm_exit_t ec = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, ec,
_("You need to specify a resource type with -t"));
return ec;
}
rc = pcmk__resource_delete(cib_conn, cib_sync_call, options.rsc_id,
options.rsc_type);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc,
_("Could not delete resource %s: %s"),
options.rsc_id, pcmk_rc_str(rc));
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_delete_param(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = cli_resource_delete_attribute(rsc, options.rsc_id,
options.prop_set,
options.attr_set_type,
options.prop_id,
options.prop_name, cib_conn,
cib_xml_orig, options.force);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_digests(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk__resource_digests(out, rsc, node, options.override_params);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_execute_agent(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
if (has_cmdline_config()) {
return cli_resource_execute_from_params(out, NULL, options.class,
options.provider, options.agent,
options.operation,
options.cmdline_params,
options.override_params,
options.timeout_ms,
args->verbosity, options.force,
options.check_level);
}
return cli_resource_execute(rsc, options.rsc_id, options.operation,
options.override_params, options.timeout_ms,
cib_conn, args->verbosity, options.force,
options.check_level);
}
static crm_exit_t
handle_fail(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = cli_resource_fail(controld_api, rsc, options.rsc_id, node);
if (rc == pcmk_rc_ok) {
// start_mainloop() sets exit_code
start_mainloop(controld_api);
return exit_code;
}
return pcmk_rc2exitc(rc);;
}
static crm_exit_t
handle_get_param(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
unsigned int count = 0;
GHashTable *params = NULL;
pcmk_node_t *current = rsc->priv->fns->active_node(rsc, &count, NULL);
bool free_params = true;
const char *value = NULL;
int rc = pcmk_rc_ok;
if (count > 1) {
out->err(out,
"%s is active on more than one node, returning the default "
"value for %s",
rsc->id, pcmk__s(options.prop_name, "unspecified property"));
current = NULL;
}
crm_debug("Looking up %s in %s", options.prop_name, rsc->id);
if (pcmk__str_eq(options.attr_set_type, PCMK_XE_INSTANCE_ATTRIBUTES,
pcmk__str_none)) {
params = pe_rsc_params(rsc, current, scheduler);
free_params = false;
value = g_hash_table_lookup(params, options.prop_name);
} else if (pcmk__str_eq(options.attr_set_type, PCMK_XE_META_ATTRIBUTES,
pcmk__str_none)) {
params = pcmk__strkey_table(free, free);
get_meta_attributes(params, rsc, NULL, scheduler);
value = g_hash_table_lookup(params, options.prop_name);
} else if (pcmk__str_eq(options.attr_set_type, ATTR_SET_ELEMENT,
pcmk__str_none)) {
value = crm_element_value(rsc->priv->xml, options.prop_name);
free_params = false;
} else {
const pcmk_rule_input_t rule_input = {
.now = scheduler->priv->now,
};
params = pcmk__strkey_table(free, free);
pe__unpack_dataset_nvpairs(rsc->priv->xml, PCMK_XE_UTILIZATION,
&rule_input, params, NULL, scheduler);
value = g_hash_table_lookup(params, options.prop_name);
}
rc = out->message(out, "attribute-list", rsc, options.prop_name, value);
if (free_params) {
g_hash_table_destroy(params);
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_active_ops(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
const char *node_name = (node != NULL)? node->priv->name : NULL;
int rc = cli_resource_print_operations(options.rsc_id, node_name, true,
scheduler);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_agents(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk__list_agents(out, options.agent_spec);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_all_ops(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
const char *node_name = (node != NULL)? node->priv->name : NULL;
int rc = cli_resource_print_operations(options.rsc_id, node_name, false,
scheduler);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_alternatives(pcmk_resource_t *rsc, pcmk_node_t *node,
cib_t *cib_conn, pcmk_scheduler_t *scheduler,
pcmk_ipc_api_t *controld_api, xmlNode *cib_xml_orig)
{
int rc = pcmk__list_alternatives(out, options.agent_spec);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_instances(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = out->message(out, "resource-names-list",
scheduler->priv->resources);
if (rc == pcmk_rc_no_output) {
// @COMPAT It seems wrong to return an error because there no resources
return CRM_EX_NOSUCH;
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_options(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
crm_exit_t ec = CRM_EX_OK;
int rc = pcmk_rc_ok;
switch (options.opt_list) {
case pcmk__opt_fencing:
rc = pcmk__list_fencing_params(out, options.all);
return pcmk_rc2exitc(rc);
case pcmk__opt_primitive:
rc = pcmk__list_primitive_meta(out, options.all);
return pcmk_rc2exitc(rc);
default:
ec = CRM_EX_SOFTWARE;
g_set_error(&error, PCMK__EXITC_ERROR, ec,
"Bug: Invalid option list type");
return ec;
}
}
static crm_exit_t
handle_list_providers(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk__list_providers(out, options.agent_spec);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_resources(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
GList *all = g_list_prepend(NULL, (gpointer) "*");
int rc = out->message(out, "resource-list", scheduler,
pcmk_show_inactive_rscs
|pcmk_show_rsc_only
|pcmk_show_pending,
true, all, all, false);
g_list_free(all);
if (rc == pcmk_rc_no_output) {
// @COMPAT It seems wrong to return an error because there no resources
return CRM_EX_NOSUCH;
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_list_standards(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk__list_standards(out);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_locate(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
GList *nodes = cli_resource_search(rsc, options.rsc_id);
int rc = out->message(out, "resource-search-list", nodes, options.rsc_id);
g_list_free_full(nodes, free);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_metadata(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk_rc_ok;
char *standard = NULL;
char *provider = NULL;
char *type = NULL;
char *metadata = NULL;
lrmd_t *lrmd_conn = NULL;
rc = lrmd__new(&lrmd_conn, NULL, NULL, 0);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__RC_ERROR, rc,
_("Could not create executor connection"));
lrmd_api_delete(lrmd_conn);
return pcmk_rc2exitc(rc);
}
rc = crm_parse_agent_spec(options.agent_spec, &standard, &provider, &type);
rc = pcmk_legacy2rc(rc);
if (rc == pcmk_rc_ok) {
rc = lrmd_conn->cmds->get_metadata(lrmd_conn, standard,
provider, type,
&metadata, 0);
rc = pcmk_legacy2rc(rc);
if (metadata != NULL) {
out->output_xml(out, PCMK_XE_METADATA, metadata);
free(metadata);
} else {
/* We were given a validly formatted spec, but it doesn't necessarily
* match up with anything that exists. Use ENXIO as the return code
* here because that maps to an exit code of CRM_EX_NOSUCH, which
* probably is the most common reason to get here.
*/
rc = ENXIO;
g_set_error(&error, PCMK__RC_ERROR, rc,
_("Metadata query for %s failed: %s"),
options.agent_spec, pcmk_rc_str(rc));
}
} else {
rc = ENXIO;
g_set_error(&error, PCMK__RC_ERROR, rc,
_("'%s' is not a valid agent specification"),
options.agent_spec);
}
lrmd_api_delete(lrmd_conn);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_move(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk_rc_ok;
if (node == NULL) {
rc = ban_or_move(out, rsc, cib_conn, options.move_lifetime);
} else {
rc = cli_resource_move(rsc, options.rsc_id, node, options.move_lifetime,
cib_conn, options.promoted_role_only,
options.force);
}
if (rc == EINVAL) {
return CRM_EX_USAGE;
}
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_query_xml(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = cli_resource_print(rsc, true);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_query_xml_raw(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = cli_resource_print(rsc, false);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_refresh(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
if (rsc == NULL) {
return refresh(out, node, controld_api);
}
refresh_resource(out, rsc, node, controld_api);
/* @FIXME Both of the calls above are supposed to set exit_code via
* start_mainloop(). But there appear to be cases in which we can return
* from refresh() or refresh_resource() without starting the mainloop or
* returning an error code. It looks as if we exit with CRM_EX_OK in those
* cases.
*/
return exit_code;
}
static crm_exit_t
handle_restart(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
/* We don't pass scheduler because rsc needs to stay valid for the entire
* lifetime of cli_resource_restart(), but it will reset and update the
* scheduler data multiple times, so it needs to use its own copy.
*/
int rc = cli_resource_restart(out, rsc, node, options.move_lifetime,
options.timeout_ms, cib_conn,
options.promoted_role_only, options.force);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_set_param(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = pcmk_rc_ok;
if (pcmk__str_empty(options.prop_value)) {
crm_exit_t ec = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, ec,
_("You need to supply a value with the -v option"));
return ec;
}
rc = cli_resource_update_attribute(rsc, options.rsc_id, options.prop_set,
options.attr_set_type, options.prop_id,
options.prop_name, options.prop_value,
options.recursive, cib_conn,
cib_xml_orig, options.force);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_wait(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = wait_till_stable(out, options.timeout_ms, cib_conn);
return pcmk_rc2exitc(rc);
}
static crm_exit_t
handle_why(pcmk_resource_t *rsc, pcmk_node_t *node, cib_t *cib_conn,
pcmk_scheduler_t *scheduler, pcmk_ipc_api_t *controld_api,
xmlNode *cib_xml_orig)
{
int rc = out->message(out, "resource-reasons-list",
scheduler->priv->resources, rsc, node);
return pcmk_rc2exitc(rc);
}
static const crm_resource_cmd_info_t crm_resource_command_info[] = {
[cmd_ban] = {
handle_ban,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_rejects_clone_instance
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_cleanup] = {
handle_cleanup,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_controller
|crm_rsc_requires_scheduler,
},
[cmd_clear] = {
handle_clear,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_rejects_clone_instance
|crm_rsc_requires_cib
|crm_rsc_requires_resource // Unless options.clear_expired
|crm_rsc_requires_scheduler,
},
[cmd_colocations] = {
handle_colocations,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_cts] = {
handle_cts,
crm_rsc_requires_cib
|crm_rsc_requires_scheduler,
},
[cmd_delete] = {
handle_delete,
crm_rsc_rejects_clone_instance
|crm_rsc_requires_cib
|crm_rsc_requires_resource,
},
[cmd_delete_param] = {
handle_delete_param,
crm_rsc_find_match_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_digests] = {
handle_digests,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_node
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_execute_agent] = {
handle_execute_agent,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_fail] = {
handle_fail,
crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_controller
|crm_rsc_requires_node
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_get_param] = {
handle_get_param,
crm_rsc_find_match_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_list_active_ops] = {
handle_list_active_ops,
crm_rsc_requires_cib
|crm_rsc_requires_scheduler,
},
[cmd_list_agents] = {
handle_list_agents,
0,
},
[cmd_list_all_ops] = {
handle_list_all_ops,
crm_rsc_requires_cib
|crm_rsc_requires_scheduler,
},
[cmd_list_alternatives] = {
handle_list_alternatives,
0,
},
[cmd_list_instances] = {
handle_list_instances,
crm_rsc_requires_cib
|crm_rsc_requires_scheduler,
},
[cmd_list_options] = {
handle_list_options,
0,
},
[cmd_list_providers] = {
handle_list_providers,
0,
},
[cmd_list_resources] = {
handle_list_resources,
crm_rsc_requires_cib
|crm_rsc_requires_scheduler,
},
[cmd_list_standards] = {
handle_list_standards,
0,
},
[cmd_locate] = {
handle_locate,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_metadata] = {
handle_metadata,
0,
},
[cmd_move] = {
handle_move,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_rejects_clone_instance
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_query_xml] = {
handle_query_xml,
crm_rsc_find_match_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_query_xml_raw] = {
handle_query_xml_raw,
crm_rsc_find_match_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_refresh] = {
handle_refresh,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_controller
|crm_rsc_requires_scheduler,
},
[cmd_restart] = {
handle_restart,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_rejects_clone_instance
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_set_param] = {
handle_set_param,
crm_rsc_find_match_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_resource
|crm_rsc_requires_scheduler,
},
[cmd_wait] = {
handle_wait,
crm_rsc_requires_cib,
},
[cmd_why] = {
handle_why,
crm_rsc_find_match_anon_basename
|crm_rsc_find_match_history
|crm_rsc_requires_cib
|crm_rsc_requires_scheduler,
},
};
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
GOptionContext *context = NULL;
GOptionEntry extra_prog_entries[] = {
{ "quiet", 'Q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &(args->quiet),
"Be less descriptive in output.",
NULL },
{ "resource", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.rsc_id,
"Resource ID",
"ID" },
{ G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &options.remainder,
NULL,
NULL },
{ NULL }
};
const char *description = "Examples:\n\n"
"List the available OCF agents:\n\n"
"\t# crm_resource --list-agents ocf\n\n"
"List the available OCF agents from the linux-ha project:\n\n"
"\t# crm_resource --list-agents ocf:heartbeat\n\n"
"Move 'myResource' to a specific node:\n\n"
"\t# crm_resource --resource myResource --move --node altNode\n\n"
"Allow (but not force) 'myResource' to move back to its original "
"location:\n\n"
"\t# crm_resource --resource myResource --clear\n\n"
"Stop 'myResource' (and anything that depends on it):\n\n"
"\t# crm_resource --resource myResource --set-parameter "
PCMK_META_TARGET_ROLE "--meta --parameter-value Stopped\n\n"
"Tell the cluster not to manage 'myResource' (the cluster will not "
"attempt to start or stop the\n"
"resource under any circumstances; useful when performing maintenance "
"tasks on a resource):\n\n"
"\t# crm_resource --resource myResource --set-parameter "
PCMK_META_IS_MANAGED "--meta --parameter-value false\n\n"
"Erase the operation history of 'myResource' on 'aNode' (the cluster "
"will 'forget' the existing\n"
"resource state, including any errors, and attempt to recover the"
"resource; useful when a resource\n"
"had failed permanently and has been repaired by an administrator):\n\n"
"\t# crm_resource --resource myResource --cleanup --node aNode\n\n";
context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
g_option_context_set_description(context, description);
/* Add the -Q option, which cannot be part of the globally supported options
* because some tools use that flag for something else.
*/
pcmk__add_main_args(context, extra_prog_entries);
pcmk__add_arg_group(context, "queries", "Queries:",
"Show query help", query_entries);
pcmk__add_arg_group(context, "commands", "Commands:",
"Show command help", command_entries);
pcmk__add_arg_group(context, "locations", "Locations:",
"Show location help", location_entries);
pcmk__add_arg_group(context, "advanced", "Advanced:",
"Show advanced option help", advanced_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
const crm_resource_cmd_info_t *command_info = NULL;
pcmk_resource_t *rsc = NULL;
pcmk_node_t *node = NULL;
cib_t *cib_conn = NULL;
pcmk_scheduler_t *scheduler = NULL;
pcmk_ipc_api_t *controld_api = NULL;
xmlNode *cib_xml_orig = NULL;
uint32_t find_flags = 0;
int rc = pcmk_rc_ok;
GOptionGroup *output_group = NULL;
gchar **processed_args = NULL;
GOptionContext *context = NULL;
/*
* Parse command line arguments
*/
args = pcmk__new_common_args(SUMMARY);
processed_args = pcmk__cmdline_preproc(argv, "GHINSTdginpstuvx");
context = build_arg_context(args, &output_group);
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
pcmk__cli_init_logging("crm_resource", args->verbosity);
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_ERROR;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("Error creating output format %s: %s"),
args->output_ty, pcmk_rc_str(rc));
goto done;
}
pe__register_messages(out);
crm_resource_register_messages(out);
lrmd__register_messages(out);
pcmk__register_lib_messages(out);
out->quiet = args->quiet;
crm_log_args(argc, argv);
/*
* Validate option combinations
*/
// --expired without --clear/-U doesn't make sense
if (options.clear_expired && (options.rsc_cmd != cmd_clear)) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, _("--expired requires --clear or -U"));
goto done;
}
if (options.remainder != NULL) {
// Commands that use positional arguments will create override_params
if (options.override_params == NULL) {
GString *msg = g_string_sized_new(128);
guint len = g_strv_length(options.remainder);
g_string_append(msg, "non-option ARGV-elements:");
for (int i = 0; i < len; i++) {
g_string_append_printf(msg, "\n[%d of %u] %s",
i + 1, len, options.remainder[i]);
}
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "%s", msg->str);
g_string_free(msg, TRUE);
goto done;
}
for (gchar **arg = options.remainder; *arg != NULL; arg++) {
gchar *name = NULL;
gchar *value = NULL;
int rc = pcmk__scan_nvpair(*arg, &name, &value);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Error parsing '%s' as a name=value pair"), *arg);
goto done;
}
g_hash_table_insert(options.override_params, name, value);
}
}
if (pcmk__str_eq(args->output_ty, "xml", pcmk__str_none)) {
switch (options.rsc_cmd) {
/* These are the only commands that have historically used the
* elements in their XML schema. For all others, use the simple list
* argument.
*/
case cmd_get_param:
case cmd_list_instances:
case cmd_list_standards:
pcmk__output_enable_list_element(out);
break;
default:
break;
}
} else if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches)) {
switch (options.rsc_cmd) {
case cmd_colocations:
case cmd_list_resources:
pcmk__output_text_set_fancy(out, true);
break;
default:
break;
}
}
if (args->version) {
out->version(out);
goto done;
}
// Ensure command is in valid range and has a handler function
if ((options.rsc_cmd >= 0) && (options.rsc_cmd <= cmd_max)) {
command_info = &crm_resource_command_info[options.rsc_cmd];
}
if ((command_info == NULL) || (command_info->fn == NULL)) {
exit_code = CRM_EX_SOFTWARE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Bug: Unimplemented command: %d"), (int) options.rsc_cmd);
goto done;
}
/* If a command-line resource agent specification was given, validate it.
* Otherwise, ensure --option was not given.
*/
if (has_cmdline_config()) {
validate_cmdline_config();
if (error != NULL) {
exit_code = CRM_EX_USAGE;
goto done;
}
} else if (options.cmdline_params != NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("--option must be used with --validate and without -r"));
g_hash_table_destroy(options.cmdline_params);
goto done;
}
// Ensure --resource is set if it's required
if (pcmk_is_set(command_info->flags, crm_rsc_requires_resource)
&& !has_cmdline_config()
&& !options.clear_expired
&& (options.rsc_id == NULL)) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Must supply a resource ID with -r/--resource"));
goto done;
}
// Ensure --node is set if it's required
if (pcmk_is_set(command_info->flags, crm_rsc_requires_node)
&& (options.host_uname == NULL)) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Must supply a node name with -N/--node"));
goto done;
}
// Establish a connection to the CIB if needed
if (pcmk_is_set(command_info->flags, crm_rsc_requires_cib)
&& !has_cmdline_config()) {
- cib_conn = cib_new();
- if ((cib_conn == NULL) || (cib_conn->cmds == NULL)) {
- exit_code = CRM_EX_DISCONNECT;
- g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
- _("Could not create CIB connection"));
- goto done;
- }
- rc = cib__signon_attempts(cib_conn, cib_command, 5);
- rc = pcmk_legacy2rc(rc);
+ rc = cib__create_signon(&cib_conn);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Could not connect to the CIB: %s"), pcmk_rc_str(rc));
goto done;
}
}
// Populate scheduler data from CIB query if needed
if (pcmk_is_set(command_info->flags, crm_rsc_requires_scheduler)
&& !has_cmdline_config()) {
rc = initialize_scheduler_data(&scheduler, cib_conn, out,
&cib_xml_orig);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
goto done;
}
}
// Establish a connection to the controller if needed
if (pcmk_is_set(command_info->flags, crm_rsc_requires_controller)
&& (getenv("CIB_file") == NULL)) {
rc = pcmk_new_ipc_api(&controld_api, pcmk_ipc_controld);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Error connecting to the controller: %s"), pcmk_rc_str(rc));
goto done;
}
pcmk_register_ipc_callback(controld_api, controller_event_callback,
&exit_code);
rc = pcmk__connect_ipc(controld_api, pcmk_ipc_dispatch_main, 5);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Error connecting to %s: %s"),
pcmk_ipc_name(controld_api, true), pcmk_rc_str(rc));
goto done;
}
}
/* Find node if --node was given.
*
* @TODO Consider stricter validation. Currently we ignore the --node
* argument for commands that don't require scheduler data, since we have no
* way to find the node in that case. This is really a usage error, but we
* don't validate strictly. We allow multiple commands (and in some cases
* their options like --node) to be specified, and we use the last one in
* case of conflicts.
*
* This isn't universally true. --expired results in a usage error unless
* the final command is --clear.
*/
if (options.host_uname != NULL) {
node = pcmk_find_node(scheduler, options.host_uname);
if (node == NULL) {
exit_code = CRM_EX_NOSUCH;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Node '%s' not found"), options.host_uname);
goto done;
}
}
/* Find resource if --resource was given and any find flags are set.
*
* @TODO Consider stricter validation. See comment above for --node.
* @TODO Setter macro for tracing?
*/
if (pcmk_is_set(command_info->flags, crm_rsc_find_match_anon_basename)) {
find_flags |= pcmk_rsc_match_anon_basename;
}
if (pcmk_is_set(command_info->flags, crm_rsc_find_match_basename)) {
find_flags |= pcmk_rsc_match_basename;
}
if (pcmk_is_set(command_info->flags, crm_rsc_find_match_history)) {
find_flags |= pcmk_rsc_match_history;
}
if ((find_flags != 0) && (options.rsc_id != NULL)) {
pcmk__assert(scheduler != NULL);
rsc = pe_find_resource_with_flags(scheduler->priv->resources,
options.rsc_id, find_flags);
if (rsc == NULL) {
exit_code = CRM_EX_NOSUCH;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Resource '%s' not found"), options.rsc_id);
goto done;
}
if (pcmk_is_set(command_info->flags, crm_rsc_rejects_clone_instance)
&& pcmk__is_clone(rsc->priv->parent)
&& (strchr(options.rsc_id, ':') != NULL)) {
exit_code = CRM_EX_INVALID_PARAM;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Cannot operate on clone resource instance '%s'"),
options.rsc_id);
goto done;
}
}
exit_code = command_info->fn(rsc, node, cib_conn, scheduler, controld_api,
cib_xml_orig);
done:
// For CRM_EX_USAGE, error is already set satisfactorily
if ((exit_code != CRM_EX_OK) && (exit_code != CRM_EX_USAGE)) {
if (error != NULL) {
char *msg = crm_strdup_printf("%s\nError performing operation: %s",
error->message, crm_exit_str(exit_code));
g_clear_error(&error);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "%s", msg);
free(msg);
} else {
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
_("Error performing operation: %s"), crm_exit_str(exit_code));
}
}
g_free(options.host_uname);
g_free(options.interval_spec);
g_free(options.move_lifetime);
g_free(options.operation);
g_free(options.prop_id);
free(options.prop_name);
g_free(options.prop_set);
g_free(options.prop_value);
g_free(options.rsc_id);
g_free(options.rsc_type);
free(options.agent_spec);
g_free(options.agent);
g_free(options.class);
g_free(options.provider);
if (options.override_params != NULL) {
g_hash_table_destroy(options.override_params);
}
g_strfreev(options.remainder);
// Don't destroy options.cmdline_params here. See comment in option_cb().
g_strfreev(processed_args);
g_option_context_free(context);
pcmk__xml_free(cib_xml_orig);
cib__clean_up_connection(&cib_conn);
pcmk_free_ipc_api(controld_api);
pcmk_free_scheduler(scheduler);
if (mainloop != NULL) {
g_main_loop_unref(mainloop);
}
pcmk__output_and_clear_error(&error, out);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
pcmk__unregister_formats();
return crm_exit(exit_code);
}
diff --git a/tools/crm_shadow.c b/tools/crm_shadow.c
index 543e113f7e..62c37320ac 100644
--- a/tools/crm_shadow.c
+++ b/tools/crm_shadow.c
@@ -1,1307 +1,1304 @@
/*
* Copyright 2004-2025 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n" \
"This command sets up an environment in which " \
"configuration tools (cibadmin,\n" \
"crm_resource, etc.) work offline instead of against a " \
"live cluster, allowing\n" \
"changes to be previewed and tested for side effects."
#define INDENT " "
enum shadow_command {
shadow_cmd_none = 0,
shadow_cmd_which,
shadow_cmd_display,
shadow_cmd_diff,
shadow_cmd_file,
shadow_cmd_create,
shadow_cmd_create_empty,
shadow_cmd_commit,
shadow_cmd_delete,
shadow_cmd_edit,
shadow_cmd_reset,
shadow_cmd_switch,
};
/*!
* \internal
* \brief Bit flags to control which fields of shadow CIB info are displayed
*
* \note Ignored for XML output.
*/
enum shadow_disp_flags {
shadow_disp_instance = (1 << 0),
shadow_disp_file = (1 << 1),
shadow_disp_content = (1 << 2),
shadow_disp_diff = (1 << 3),
};
static crm_exit_t exit_code = CRM_EX_OK;
static struct {
enum shadow_command cmd;
int cmd_options;
char *instance;
gboolean force;
gboolean batch;
gboolean full_upload;
gchar *validate_with;
} options = {
.cmd_options = cib_sync_call,
};
/*!
* \internal
* \brief Display an instruction to the user
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instructional message
*/
PCMK__OUTPUT_ARGS("instruction", "const char *")
static int
instruction_default(pcmk__output_t *out, va_list args)
{
const char *msg = va_arg(args, const char *);
if (msg == NULL) {
return pcmk_rc_no_output;
}
return out->info(out, "%s", msg);
}
/*!
* \internal
* \brief Display an instruction to the user
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instructional message
*/
PCMK__OUTPUT_ARGS("instruction", "const char *")
static int
instruction_xml(pcmk__output_t *out, va_list args)
{
const char *msg = va_arg(args, const char *);
if (msg == NULL) {
return pcmk_rc_no_output;
}
pcmk__output_create_xml_text_node(out, "instruction", msg);
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Display information about a shadow CIB instance
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instance name (can be \p NULL)
* -# Shadow file name (can be \p NULL)
* -# Shadow file content (can be \p NULL)
* -# Patchset containing the changes in the shadow CIB (can be \p NULL)
* -# Group of \p shadow_disp_flags indicating which fields to display
*/
PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
"const xmlNode *", "enum shadow_disp_flags")
static int
shadow_default(pcmk__output_t *out, va_list args)
{
const char *instance = va_arg(args, const char *);
const char *filename = va_arg(args, const char *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
int rc = pcmk_rc_no_output;
if (pcmk_is_set(flags, shadow_disp_instance)) {
rc = out->info(out, "Instance: %s", pcmk__s(instance, ""));
}
if (pcmk_is_set(flags, shadow_disp_file)) {
rc = out->info(out, "File name: %s", pcmk__s(filename, ""));
}
if (pcmk_is_set(flags, shadow_disp_content)) {
rc = out->info(out, "Content:");
if (content != NULL) {
GString *buf = g_string_sized_new(1024);
gchar *str = NULL;
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
buf, 0);
str = g_string_free(buf, FALSE);
str = pcmk__trim(str);
if (!pcmk__str_empty(str)) {
out->info(out, "%s", str);
}
g_free(str);
} else {
out->info(out, "");
}
}
if (pcmk_is_set(flags, shadow_disp_diff)) {
rc = out->info(out, "Diff:");
if (diff != NULL) {
out->message(out, "xml-patchset", diff);
} else {
out->info(out, "");
}
}
return rc;
}
/*!
* \internal
* \brief Display information about a shadow CIB instance
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instance name (can be \p NULL)
* -# Shadow file name (can be \p NULL)
* -# Shadow file content (can be \p NULL)
* -# Patchset containing the changes in the shadow CIB (can be \p NULL)
* -# Group of \p shadow_disp_flags indicating which fields to display
*/
PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
"const xmlNode *", "enum shadow_disp_flags")
static int
shadow_text(pcmk__output_t *out, va_list args)
{
if (!out->is_quiet(out)) {
return shadow_default(out, args);
} else {
const char *instance = va_arg(args, const char *);
const char *filename = va_arg(args, const char *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
int rc = pcmk_rc_no_output;
bool quiet_orig = out->quiet;
/* We have to disable quiet mode for the "xml-patchset" message if we
* call it, so we might as well do so for this whole section.
*/
out->quiet = false;
if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) {
rc = out->info(out, "%s", instance);
}
if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) {
rc = out->info(out, "%s", filename);
}
if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) {
GString *buf = g_string_sized_new(1024);
gchar *str = NULL;
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
buf, 0);
str = g_string_free(buf, FALSE);
str = pcmk__trim(str);
rc = out->info(out, "%s", str);
g_free(str);
}
if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) {
rc = out->message(out, "xml-patchset", diff);
}
out->quiet = quiet_orig;
return rc;
}
}
/*!
* \internal
* \brief Display information about a shadow CIB instance
*
* \param[in,out] out Output object
* \param[in] args Message-specific arguments
*
* \return Standard Pacemaker return code
*
* \note \p args should contain the following:
* -# Instance name (can be \p NULL)
* -# Shadow file name (can be \p NULL)
* -# Shadow file content (can be \p NULL)
* -# Patchset containing the changes in the shadow CIB (can be \p NULL)
* -# Group of \p shadow_disp_flags indicating which fields to display
* (ignored)
*/
PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
"const xmlNode *", "enum shadow_disp_flags")
static int
shadow_xml(pcmk__output_t *out, va_list args)
{
const char *instance = va_arg(args, const char *);
const char *filename = va_arg(args, const char *);
const xmlNode *content = va_arg(args, const xmlNode *);
const xmlNode *diff = va_arg(args, const xmlNode *);
enum shadow_disp_flags flags G_GNUC_UNUSED =
(enum shadow_disp_flags) va_arg(args, int);
pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW,
PCMK_XA_INSTANCE, instance,
PCMK_XA_FILE, filename,
NULL);
if (content != NULL) {
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
0);
out->output_xml(out, PCMK_XE_CONTENT, buf->str);
g_string_free(buf, TRUE);
}
if (diff != NULL) {
out->message(out, "xml-patchset", diff);
}
pcmk__output_xml_pop_parent(out);
return pcmk_rc_ok;
}
static const pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
static const pcmk__message_entry_t fmt_functions[] = {
{ "instruction", "default", instruction_default },
{ "instruction", "xml", instruction_xml },
{ "shadow", "default", shadow_default },
{ "shadow", "text", shadow_text },
{ "shadow", "xml", shadow_xml },
{ NULL, NULL, NULL }
};
/*!
* \internal
* \brief Set the error when \p --force is not passed with a dangerous command
*
* \param[in] reason Why command is dangerous
* \param[in] for_shadow If true, command is dangerous to the shadow file.
* Otherwise, command is dangerous to the active
* cluster.
* \param[in] show_mismatch If true and the supplied shadow instance is not
* the same as the active shadow instance, report
* this
* \param[out] error Where to store error
*/
static void
set_danger_error(const char *reason, bool for_shadow, bool show_mismatch,
GError **error)
{
const char *active = getenv("CIB_shadow");
char *full = NULL;
if (show_mismatch
&& !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) {
full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow "
"instance (%s) is not the same as the active "
"one (%s)",
reason, options.instance, active);
reason = full;
}
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"%s%sTo prevent accidental destruction of the %s, the --force "
"flag is required in order to proceed.",
pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""),
(for_shadow? "shadow file" : "cluster"));
free(full);
}
/*!
* \internal
* \brief Get the active shadow instance from the environment
*
* This sets \p options.instance to the value of the \p CIB_shadow env variable.
*
* \param[out] error Where to store error
*/
static int
get_instance_from_env(GError **error)
{
int rc = pcmk_rc_ok;
pcmk__str_update(&options.instance, getenv("CIB_shadow"));
if (options.instance == NULL) {
rc = ENXIO;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"No active shadow configuration defined");
}
return rc;
}
/*!
* \internal
* \brief Validate that the shadow file does or does not exist, as appropriate
*
* \param[in] filename Absolute path of shadow file
* \param[in] should_exist Whether the shadow file is expected to exist
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
check_file_exists(const char *filename, bool should_exist, GError **error)
{
struct stat buf;
if (!should_exist && (stat(filename, &buf) == 0)) {
char *reason = crm_strdup_printf("A shadow instance '%s' already "
"exists", options.instance);
exit_code = CRM_EX_CANTCREAT;
set_danger_error(reason, true, false, error);
free(reason);
return EEXIST;
}
if (should_exist && (stat(filename, &buf) < 0)) {
int rc = errno;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not access shadow instance '%s': %s",
options.instance, strerror(rc));
return errno;
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Connect to the "real" (non-shadow) CIB
*
* \param[out] real_cib Where to store CIB connection
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
connect_real_cib(cib_t **real_cib, GError **error)
{
+ const char *active = getenv("CIB_shadow");
int rc = pcmk_rc_ok;
- *real_cib = cib_new_no_shadow();
- if (*real_cib == NULL) {
- rc = ENOMEM;
- exit_code = pcmk_rc2exitc(rc);
- g_set_error(error, PCMK__EXITC_ERROR, exit_code,
- "Could not create a CIB connection object");
- return rc;
+ // Create a non-shadowed CIB connection object and then restore CIB_shadow
+ unsetenv("CIB_shadow");
+ rc = cib__create_signon(real_cib);
+ if (active != NULL) {
+ setenv("CIB_shadow", active, 1);
}
- rc = cib__signon_attempts(*real_cib, cib_command, 5);
- rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not connect to CIB: %s", pcmk_rc_str(rc));
}
return rc;
}
/*!
* \internal
* \brief Query the "real" (non-shadow) CIB and store the result
*
* \param[out] output Where to store query output
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
query_real_cib(xmlNode **output, GError **error)
{
cib_t *real_cib = NULL;
int rc = connect_real_cib(&real_cib, error);
if (rc != pcmk_rc_ok) {
goto done;
}
rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not query the non-shadow CIB: %s", pcmk_rc_str(rc));
}
done:
cib_delete(real_cib);
return rc;
}
/*!
* \internal
* \brief Read XML from the given file
*
* \param[in] filename Path of input file
* \param[out] output Where to store XML read from \p filename
* \param[out] error Where to store error
*
* \return Standard Pacemaker return code
*/
static int
read_xml(const char *filename, xmlNode **output, GError **error)
{
int rc = pcmk_rc_ok;
*output = pcmk__xml_read(filename);
if (*output == NULL) {
rc = pcmk_rc_no_input;
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not parse XML from input file '%s'", filename);
}
return rc;
}
/*!
* \internal
* \brief Write the shadow XML to a file
*
* \param[in] xml Shadow XML
* \param[in] filename Name of destination file
* \param[in] reset Whether the write is a reset (for logging only)
* \param[out] error Where to store error
*/
static int
write_shadow_file(const xmlNode *xml, const char *filename, bool reset,
GError **error)
{
int rc = pcmk__xml_write_file(xml, filename, false);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not %s the shadow instance '%s': %s",
reset? "reset" : "create", options.instance,
pcmk_rc_str(rc));
}
return rc;
}
/*!
* \internal
* \brief Create a shell prompt based on the given shadow instance name
*
* \return Newly created prompt
*
* \note The caller is responsible for freeing the return value using \p free().
*/
static inline char *
get_shadow_prompt(void)
{
return crm_strdup_printf("shadow[%.40s] # ", options.instance);
}
/*!
* \internal
* \brief Set up environment variables for a shadow instance
*
* \param[in,out] out Output object
* \param[in] do_switch If true, switch to an existing instance (logging
* only)
* \param[out] error Where to store error
*/
static void
shadow_setup(pcmk__output_t *out, bool do_switch, GError **error)
{
const char *active = getenv("CIB_shadow");
const char *prompt = getenv("PS1");
const char *shell = getenv("SHELL");
char *new_prompt = get_shadow_prompt();
if (pcmk__str_eq(active, options.instance, pcmk__str_none)
&& pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) {
// CIB_shadow and prompt environment variables are already set up
goto done;
}
if (!options.batch && (shell != NULL)) {
out->info(out, "Setting up shadow instance");
setenv("PS1", new_prompt, 1);
setenv("CIB_shadow", options.instance, 1);
out->message(out, PCMK_XE_INSTRUCTION,
"Press Ctrl+D to exit the crm_shadow shell");
if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) {
execl(shell, shell, "--norc", "--noprofile", NULL);
} else {
execl(shell, shell, NULL);
}
exit_code = pcmk_rc2exitc(errno);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Failed to launch shell '%s': %s",
shell, pcmk_rc_str(errno));
} else {
char *msg = NULL;
const char *prefix = "A new shadow instance was created. To begin "
"using it";
if (do_switch) {
prefix = "To switch to the named shadow instance";
}
msg = crm_strdup_printf("%s, enter the following into your shell:\n"
"\texport CIB_shadow=%s",
prefix, options.instance);
out->message(out, "instruction", msg);
free(msg);
}
done:
free(new_prompt);
}
/*!
* \internal
* \brief Remind the user to clean up the shadow environment
*
* \param[in,out] out Output object
*/
static void
shadow_teardown(pcmk__output_t *out)
{
const char *active = getenv("CIB_shadow");
const char *prompt = getenv("PS1");
if (pcmk__str_eq(active, options.instance, pcmk__str_none)) {
char *our_prompt = get_shadow_prompt();
if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) {
out->message(out, "instruction",
"Press Ctrl+D to exit the crm_shadow shell");
} else {
out->message(out, "instruction",
"Remember to unset the CIB_shadow variable by "
"entering the following into your shell:\n"
"\tunset CIB_shadow");
}
free(our_prompt);
}
}
/*!
* \internal
* \brief Commit the shadow file contents to the active cluster
*
* \param[out] error Where to store error
*/
static void
commit_shadow_file(GError **error)
{
char *filename = NULL;
cib_t *real_cib = NULL;
xmlNodePtr input = NULL;
xmlNodePtr section_xml = NULL;
const char *section = NULL;
int rc = pcmk_rc_ok;
if (!options.force) {
const char *reason = "The commit command overwrites the active cluster "
"configuration";
exit_code = CRM_EX_USAGE;
set_danger_error(reason, false, true, error);
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
goto done;
}
if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) {
goto done;
}
if (read_xml(filename, &input, error) != pcmk_rc_ok) {
goto done;
}
section_xml = input;
if (!options.full_upload) {
section = PCMK_XE_CONFIGURATION;
section_xml = pcmk__xe_first_child(input, section, NULL, NULL);
}
rc = real_cib->cmds->replace(real_cib, section, section_xml,
options.cmd_options);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not commit shadow instance '%s' to the CIB: %s",
options.instance, pcmk_rc_str(rc));
}
done:
free(filename);
cib_delete(real_cib);
pcmk__xml_free(input);
}
/*!
* \internal
* \brief Create a new empty shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*
* \note If \p --force is given, we try to write the file regardless of whether
* it already exists.
*/
static void
create_shadow_empty(pcmk__output_t *out, GError **error)
{
char *filename = get_shadow_file(options.instance);
xmlNode *output = NULL;
if (!options.force
&& (check_file_exists(filename, false, error) != pcmk_rc_ok)) {
goto done;
}
output = createEmptyCib(0);
crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
out->info(out, "Created new %s configuration",
crm_element_value(output, PCMK_XA_VALIDATE_WITH));
if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
goto done;
}
shadow_setup(out, false, error);
done:
free(filename);
pcmk__xml_free(output);
}
/*!
* \internal
* \brief Create a shadow instance based on the active CIB
*
* \param[in,out] out Output object
* \param[in] reset If true, overwrite the given existing shadow instance.
* Otherwise, create a new shadow instance with the given
* name.
* \param[out] error Where to store error
*
* \note If \p --force is given, we try to write the file regardless of whether
* it already exists.
*/
static void
create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error)
{
char *filename = get_shadow_file(options.instance);
xmlNode *output = NULL;
if (!options.force) {
if (reset) {
const char *reason = "The reset command overwrites the active "
"shadow configuration";
exit_code = CRM_EX_USAGE;
set_danger_error(reason, true, true, error);
goto done;
}
if (check_file_exists(filename, reset, error) != pcmk_rc_ok) {
goto done;
}
}
if (query_real_cib(&output, error) != pcmk_rc_ok) {
goto done;
}
if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) {
goto done;
}
shadow_setup(out, false, error);
done:
free(filename);
pcmk__xml_free(output);
}
/*!
* \internal
* \brief Delete the shadow file
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
delete_shadow_file(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
if (!options.force) {
const char *reason = "The delete command removes the specified shadow "
"file";
exit_code = CRM_EX_USAGE;
set_danger_error(reason, true, true, error);
return;
}
filename = get_shadow_file(options.instance);
if ((unlink(filename) < 0) && (errno != ENOENT)) {
exit_code = pcmk_rc2exitc(errno);
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not remove shadow instance '%s': %s",
options.instance, strerror(errno));
} else {
shadow_teardown(out);
}
free(filename);
}
/*!
* \internal
* \brief Open the shadow file in a text editor
*
* \param[out] error Where to store error
*
* \note The \p EDITOR environment variable must be set.
*/
static void
edit_shadow_file(GError **error)
{
char *filename = NULL;
const char *editor = NULL;
if (get_instance_from_env(error) != pcmk_rc_ok) {
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
goto done;
}
editor = getenv("EDITOR");
if (editor == NULL) {
exit_code = CRM_EX_NOT_CONFIGURED;
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"No value for EDITOR defined");
goto done;
}
execlp(editor, "--", filename, NULL);
exit_code = CRM_EX_OSFILE;
g_set_error(error, PCMK__EXITC_ERROR, exit_code,
"Could not invoke EDITOR (%s %s): %s",
editor, filename, strerror(errno));
done:
free(filename);
}
/*!
* \internal
* \brief Show the contents of the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_contents(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
if (get_instance_from_env(error) != pcmk_rc_ok) {
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
xmlNode *output = NULL;
bool quiet_orig = out->quiet;
if (read_xml(filename, &output, error) != pcmk_rc_ok) {
goto done;
}
out->quiet = true;
out->message(out, "shadow",
options.instance, NULL, output, NULL, shadow_disp_content);
out->quiet = quiet_orig;
pcmk__xml_free(output);
}
done:
free(filename);
}
/*!
* \internal
* \brief Show the changes in the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_diff(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
xmlNodePtr old_config = NULL;
xmlNodePtr new_config = NULL;
xmlNodePtr diff = NULL;
bool quiet_orig = out->quiet;
if (get_instance_from_env(error) != pcmk_rc_ok) {
return;
}
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
goto done;
}
if (query_real_cib(&old_config, error) != pcmk_rc_ok) {
goto done;
}
if (read_xml(filename, &new_config, error) != pcmk_rc_ok) {
goto done;
}
pcmk__xml_mark_changes(old_config, new_config);
diff = xml_create_patchset(0, old_config, new_config, NULL, false);
pcmk__log_xml_changes(LOG_INFO, new_config);
pcmk__xml_commit_changes(new_config->doc);
out->quiet = true;
out->message(out, "shadow",
options.instance, NULL, NULL, diff, shadow_disp_diff);
out->quiet = quiet_orig;
if (diff != NULL) {
/* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an error; we
* just want to indicate that there are differences (as the diff command
* does).
*/
exit_code = CRM_EX_ERROR;
}
done:
free(filename);
pcmk__xml_free(old_config);
pcmk__xml_free(new_config);
pcmk__xml_free(diff);
}
/*!
* \internal
* \brief Show the absolute path of the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_filename(pcmk__output_t *out, GError **error)
{
if (get_instance_from_env(error) == pcmk_rc_ok) {
char *filename = get_shadow_file(options.instance);
bool quiet_orig = out->quiet;
out->quiet = true;
out->message(out, "shadow",
options.instance, filename, NULL, NULL, shadow_disp_file);
out->quiet = quiet_orig;
free(filename);
}
}
/*!
* \internal
* \brief Show the active shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
show_shadow_instance(pcmk__output_t *out, GError **error)
{
if (get_instance_from_env(error) == pcmk_rc_ok) {
bool quiet_orig = out->quiet;
out->quiet = true;
out->message(out, "shadow",
options.instance, NULL, NULL, NULL, shadow_disp_instance);
out->quiet = quiet_orig;
}
}
/*!
* \internal
* \brief Switch to the given shadow instance
*
* \param[in,out] out Output object
* \param[out] error Where to store error
*/
static void
switch_shadow_instance(pcmk__output_t *out, GError **error)
{
char *filename = NULL;
filename = get_shadow_file(options.instance);
if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
shadow_setup(out, true, error);
}
free(filename);
}
static gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
GError **error)
{
if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) {
options.cmd = shadow_cmd_which;
} else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) {
options.cmd = shadow_cmd_display;
} else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) {
options.cmd = shadow_cmd_diff;
} else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) {
options.cmd = shadow_cmd_file;
} else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) {
options.cmd = shadow_cmd_create;
} else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) {
options.cmd = shadow_cmd_create_empty;
} else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) {
options.cmd = shadow_cmd_commit;
} else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
options.cmd = shadow_cmd_delete;
} else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) {
options.cmd = shadow_cmd_edit;
} else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) {
options.cmd = shadow_cmd_reset;
} else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) {
options.cmd = shadow_cmd_switch;
} else {
// Should be impossible
return FALSE;
}
// optarg may be NULL and that's okay
pcmk__str_update(&options.instance, optarg);
return TRUE;
}
static GOptionEntry query_entries[] = {
{ "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Indicate the active shadow copy", NULL },
{ "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the contents of the active shadow copy", NULL },
{ "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the changes in the active shadow copy", NULL },
{ "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the location of the active shadow copy file", NULL },
{ NULL }
};
static GOptionEntry command_entries[] = {
{ "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Create the named shadow copy of the active cluster configuration",
"name" },
{ "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
command_cb,
"Create the named shadow copy with an empty cluster configuration.\n"
INDENT "Optional: --validate-with", "name" },
{ "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Upload the contents of the named shadow copy to the cluster", "name" },
{ "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Delete the contents of the named shadow copy", "name" },
{ "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Edit the contents of the active shadow copy with your favorite $EDITOR",
NULL },
{ "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"Recreate named shadow copy from the active cluster configuration",
"name. Required: --force." },
{ "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
"(Advanced) Switch to the named shadow copy", "name" },
{ NULL }
};
static GOptionEntry addl_entries[] = {
{ "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
"(Advanced) Force the action to be performed", NULL },
{ "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch,
"(Advanced) Don't spawn a new shell", NULL },
{ "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload,
"(Advanced) Upload entire CIB, including status, with --commit", NULL },
{ "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
&options.validate_with,
"(Advanced) Create an older configuration version", NULL },
{ NULL }
};
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
{
const char *desc = NULL;
GOptionContext *context = NULL;
desc = "Examples:\n\n"
"Create a blank shadow configuration:\n\n"
"\t# crm_shadow --create-empty myShadow\n\n"
"Create a shadow configuration from the running cluster\n\n"
"\t# crm_shadow --create myShadow\n\n"
"Display the current shadow configuration:\n\n"
"\t# crm_shadow --display\n\n"
"Discard the current shadow configuration (named myShadow):\n\n"
"\t# crm_shadow --delete myShadow --force\n\n"
"Upload current shadow configuration (named myShadow) to running "
"cluster:\n\n"
"\t# crm_shadow --commit myShadow\n\n";
context = pcmk__build_arg_context(args, "text (default), xml", group,
"|");
g_option_context_set_description(context, desc);
pcmk__add_arg_group(context, "queries", "Queries:",
"Show query help", query_entries);
pcmk__add_arg_group(context, "commands", "Commands:",
"Show command help", command_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
int rc = pcmk_rc_ok;
pcmk__output_t *out = NULL;
GError *error = NULL;
GOptionGroup *output_group = NULL;
pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
gchar **processed_args = pcmk__cmdline_preproc(argv, "CDcersv");
GOptionContext *context = build_arg_context(args, &output_group);
crm_log_preinit(NULL, argc, argv);
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = CRM_EX_ERROR;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error creating output format %s: %s", args->output_ty,
pcmk_rc_str(rc));
goto done;
}
if (g_strv_length(processed_args) > 1) {
gchar *help = g_option_context_get_help(context, TRUE, NULL);
GString *extra = g_string_sized_new(128);
for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
if (extra->len > 0) {
g_string_append_c(extra, ' ');
}
g_string_append(extra, processed_args[lpc]);
}
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"non-option ARGV-elements: %s\n\n%s", extra->str, help);
g_free(help);
g_string_free(extra, TRUE);
goto done;
}
if (args->version) {
out->version(out);
goto done;
}
pcmk__register_messages(out, fmt_functions);
if (options.cmd == shadow_cmd_none) {
// @COMPAT: Create a default command if other tools have one
gchar *help = g_option_context_get_help(context, TRUE, NULL);
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must specify a query or command option\n\n%s", help);
g_free(help);
goto done;
}
pcmk__cli_init_logging("crm_shadow", args->verbosity);
if (args->verbosity > 0) {
cib__set_call_options(options.cmd_options, crm_system_name,
cib_verbose);
}
// Run the command
switch (options.cmd) {
case shadow_cmd_commit:
commit_shadow_file(&error);
break;
case shadow_cmd_create:
create_shadow_from_cib(out, false, &error);
break;
case shadow_cmd_create_empty:
create_shadow_empty(out, &error);
break;
case shadow_cmd_reset:
create_shadow_from_cib(out, true, &error);
break;
case shadow_cmd_delete:
delete_shadow_file(out, &error);
break;
case shadow_cmd_diff:
show_shadow_diff(out, &error);
break;
case shadow_cmd_display:
show_shadow_contents(out, &error);
break;
case shadow_cmd_edit:
edit_shadow_file(&error);
break;
case shadow_cmd_file:
show_shadow_filename(out, &error);
break;
case shadow_cmd_switch:
switch_shadow_instance(out, &error);
break;
case shadow_cmd_which:
show_shadow_instance(out, &error);
break;
default:
// Should never reach this point
break;
}
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
pcmk__output_and_clear_error(&error, out);
free(options.instance);
g_free(options.validate_with);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
crm_exit(exit_code);
}
diff --git a/tools/crm_ticket.c b/tools/crm_ticket.c
index 4efff664cd..7e2e717e7c 100644
--- a/tools/crm_ticket.c
+++ b/tools/crm_ticket.c
@@ -1,666 +1,657 @@
/*
* Copyright 2012-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
GError *error = NULL;
#define SUMMARY "Perform tasks related to cluster tickets\n\n" \
"Allows ticket attributes to be queried, modified and deleted."
struct {
gchar *attr_default;
gchar *attr_id;
char *attr_name;
char *attr_value;
gboolean force;
char *get_attr_name;
gboolean quiet;
gchar *set_name;
char ticket_cmd;
gchar *ticket_id;
gchar *xml_file;
} options = {
.ticket_cmd = 'S'
};
GList *attr_delete;
GHashTable *attr_set;
bool modified = false;
int cib_options = cib_sync_call;
static pcmk__output_t *out = NULL;
#define INDENT " "
static pcmk__supported_format_t formats[] = {
PCMK__SUPPORTED_FORMAT_NONE,
PCMK__SUPPORTED_FORMAT_TEXT,
PCMK__SUPPORTED_FORMAT_XML,
{ NULL, NULL, NULL }
};
static gboolean
attr_value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
pcmk__str_update(&options.attr_value, optarg);
if (!options.attr_name || !options.attr_value) {
return TRUE;
}
pcmk__insert_dup(attr_set, options.attr_name, options.attr_value);
pcmk__str_update(&options.attr_name, NULL);
pcmk__str_update(&options.attr_value, NULL);
modified = true;
return TRUE;
}
static gboolean
command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
if (pcmk__str_any_of(option_name, "--info", "-l", NULL)) {
options.ticket_cmd = 'l';
} else if (pcmk__str_any_of(option_name, "--details", "-L", NULL)) {
options.ticket_cmd = 'L';
} else if (pcmk__str_any_of(option_name, "--raw", "-w", NULL)) {
options.ticket_cmd = 'w';
} else if (pcmk__str_any_of(option_name, "--query-xml", "-q", NULL)) {
options.ticket_cmd = 'q';
} else if (pcmk__str_any_of(option_name, "--constraints", "-c", NULL)) {
options.ticket_cmd = 'c';
} else if (pcmk__str_any_of(option_name, "--cleanup", "-C", NULL)) {
options.ticket_cmd = 'C';
}
return TRUE;
}
static gboolean
delete_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
attr_delete = g_list_append(attr_delete, strdup(optarg));
modified = true;
return TRUE;
}
static gboolean
get_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
pcmk__str_update(&options.get_attr_name, optarg);
options.ticket_cmd = 'G';
return TRUE;
}
static gboolean
grant_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
if (pcmk__str_any_of(option_name, "--grant", "-g", NULL)) {
pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_TRUE);
modified = true;
} else if (pcmk__str_any_of(option_name, "--revoke", "-r", NULL)) {
pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_FALSE);
modified = true;
} else if (pcmk__str_any_of(option_name, "--standby", "-s", NULL)) {
pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_TRUE);
modified = true;
} else if (pcmk__str_any_of(option_name, "--activate", "-a", NULL)) {
pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_FALSE);
modified = true;
}
return TRUE;
}
static gboolean
set_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) {
pcmk__str_update(&options.attr_name, optarg);
if (!options.attr_name || !options.attr_value) {
return TRUE;
}
pcmk__insert_dup(attr_set, options.attr_name, options.attr_value);
pcmk__str_update(&options.attr_name, NULL);
pcmk__str_update(&options.attr_value, NULL);
modified = true;
return TRUE;
}
static GOptionEntry query_entries[] = {
{ "info", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the information of ticket(s)",
NULL },
{ "details", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the details of ticket(s)",
NULL },
{ "raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the IDs of ticket(s)",
NULL },
{ "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Query the XML of ticket(s)",
NULL },
{ "constraints", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Display the " PCMK_XE_RSC_TICKET " constraints that apply to ticket(s)",
NULL },
{ NULL }
};
static GOptionEntry command_entries[] = {
{ "grant", 'g', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
"Grant a ticket to this cluster site",
NULL },
{ "revoke", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
"Revoke a ticket from this cluster site",
NULL },
{ "standby", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
"Tell this cluster site this ticket is standby",
NULL },
{ "activate", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb,
"Tell this cluster site this ticket is active",
NULL },
{ NULL }
};
static GOptionEntry advanced_entries[] = {
{ "get-attr", 'G', 0, G_OPTION_ARG_CALLBACK, get_attr_cb,
"Display the named attribute for a ticket",
"ATTRIBUTE" },
{ "set-attr", 'S', 0, G_OPTION_ARG_CALLBACK, set_attr_cb,
"Set the named attribute for a ticket",
"ATTRIBUTE" },
{ "delete-attr", 'D', 0, G_OPTION_ARG_CALLBACK, delete_attr_cb,
"Delete the named attribute for a ticket",
"ATTRIBUTE" },
{ "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
"Delete all state of a ticket at this cluster site",
NULL },
{ NULL}
};
static GOptionEntry addl_entries[] = {
{ "attr-value", 'v', 0, G_OPTION_ARG_CALLBACK, attr_value_cb,
"Attribute value to use with -S",
"VALUE" },
{ "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default,
"(Advanced) Default attribute value to display if none is found\n"
INDENT "(for use with -G)",
"VALUE" },
{ "force", 'f', 0, G_OPTION_ARG_NONE, &options.force,
"(Advanced) Force the action to be performed",
NULL },
{ "ticket", 't', 0, G_OPTION_ARG_STRING, &options.ticket_id,
"Ticket ID",
"ID" },
{ "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.xml_file,
NULL,
NULL },
{ NULL }
};
static GOptionEntry deprecated_entries[] = {
{ "set-name", 'n', 0, G_OPTION_ARG_STRING, &options.set_name,
"(Advanced) ID of the " PCMK_XE_INSTANCE_ATTRIBUTES " object to change",
"ID" },
{ "nvpair", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id,
"(Advanced) ID of the nvpair object to change/delete",
"ID" },
{ "quiet", 'Q', 0, G_OPTION_ARG_NONE, &options.quiet,
"Print only the value on stdout",
NULL },
{ NULL }
};
static void
ticket_grant_warning(gchar *ticket_id)
{
out->err(out, "This command cannot help you verify whether '%s' has "
"been already granted elsewhere.\n"
"If you really want to grant '%s' to this site now, and "
"you know what you are doing,\n"
"please specify --force.",
ticket_id, ticket_id);
}
static void
ticket_revoke_warning(gchar *ticket_id)
{
out->err(out, "Revoking '%s' can trigger the specified '" PCMK_XA_LOSS_POLICY
"'(s) relating to '%s'.\n\n"
"You can check that with:\n"
"crm_ticket --ticket %s --constraints\n\n"
"Otherwise before revoking '%s', you may want to make '%s'"
"standby with:\n"
"crm_ticket --ticket %s --standby\n\n"
"If you really want to revoke '%s' from this site now, and "
"you know what you are doing,\n"
"please specify --force.",
ticket_id, ticket_id, ticket_id, ticket_id, ticket_id,
ticket_id, ticket_id);
}
static GOptionContext *
build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
{
GOptionContext *context = NULL;
const char *description = "Examples:\n\n"
"Display the info of tickets:\n\n"
"\tcrm_ticket --info\n\n"
"Display the detailed info of tickets:\n\n"
"\tcrm_ticket --details\n\n"
"Display the XML of 'ticketA':\n\n"
"\tcrm_ticket --ticket ticketA --query-xml\n\n"
"Display the " PCMK_XE_RSC_TICKET " constraints that apply to 'ticketA':\n\n"
"\tcrm_ticket --ticket ticketA --constraints\n\n"
"Grant 'ticketA' to this cluster site:\n\n"
"\tcrm_ticket --ticket ticketA --grant\n\n"
"Revoke 'ticketA' from this cluster site:\n\n"
"\tcrm_ticket --ticket ticketA --revoke\n\n"
"Make 'ticketA' standby (the cluster site will treat a granted\n"
"'ticketA' as 'standby', and the dependent resources will be\n"
"stopped or demoted gracefully without triggering loss-policies):\n\n"
"\tcrm_ticket --ticket ticketA --standby\n\n"
"Activate 'ticketA' from being standby:\n\n"
"\tcrm_ticket --ticket ticketA --activate\n\n"
"Get the value of the 'granted' attribute for 'ticketA':\n\n"
"\tcrm_ticket --ticket ticketA --get-attr granted\n\n"
"Set the value of the 'standby' attribute for 'ticketA':\n\n"
"\tcrm_ticket --ticket ticketA --set-attr standby --attr-value true\n\n"
"Delete the 'granted' attribute for 'ticketA':\n\n"
"\tcrm_ticket --ticket ticketA --delete-attr granted\n\n"
"Erase the operation history of 'ticketA' at this cluster site,\n"
"causing the cluster site to 'forget' the existing ticket state:\n\n"
"\tcrm_ticket --ticket ticketA --cleanup\n\n";
context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
g_option_context_set_description(context, description);
pcmk__add_arg_group(context, "queries", "Queries:",
"Show queries", query_entries);
pcmk__add_arg_group(context, "commands", "Commands:",
"Show command options", command_entries);
pcmk__add_arg_group(context, "advanced", "Advanced Options:",
"Show advanced options", advanced_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
pcmk__add_arg_group(context, "deprecated", "Deprecated Options:",
"Show deprecated options", deprecated_entries);
return context;
}
int
main(int argc, char **argv)
{
pcmk_scheduler_t *scheduler = NULL;
xmlNode *cib_xml_copy = NULL;
cib_t *cib_conn = NULL;
crm_exit_t exit_code = CRM_EX_OK;
int rc = pcmk_rc_ok;
GOptionGroup *output_group = NULL;
pcmk__common_args_t *args = NULL;
GOptionContext *context = NULL;
gchar **processed_args = NULL;
attr_set = pcmk__strkey_table(free, free);
attr_delete = NULL;
args = pcmk__new_common_args(SUMMARY);
context = build_arg_context(args, &output_group);
processed_args = pcmk__cmdline_preproc(argv, "dintvxCDGS");
pcmk__register_formats(output_group, formats);
if (!g_option_context_parse_strv(context, &processed_args, &error)) {
exit_code = CRM_EX_USAGE;
goto done;
}
pcmk__cli_init_logging("crm_ticket", args->verbosity);
rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Error creating output format %s: %s", args->output_ty,
pcmk_rc_str(rc));
goto done;
}
pe__register_messages(out);
pcmk__register_lib_messages(out);
if (args->version) {
out->version(out);
goto done;
}
scheduler = pcmk_new_scheduler();
if (scheduler == NULL) {
rc = errno;
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not allocate scheduler data: %s", pcmk_rc_str(rc));
goto done;
}
pcmk__set_scheduler_flags(scheduler, pcmk__sched_no_counts);
- cib_conn = cib_new();
- if (cib_conn == NULL) {
- exit_code = CRM_EX_DISCONNECT;
- g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB manager");
- goto done;
- }
-
- rc = cib__signon_attempts(cib_conn, cib_command, 5);
- rc = pcmk_legacy2rc(rc);
-
+ rc = cib__create_signon(&cib_conn);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB: %s",
pcmk_rc_str(rc));
goto done;
}
if (options.xml_file != NULL) {
cib_xml_copy = pcmk__xml_read(options.xml_file);
} else {
rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml_copy,
cib_sync_call);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get local CIB: %s",
pcmk_rc_str(rc));
goto done;
}
}
rc = pcmk__update_configured_schema(&cib_xml_copy, false);
if (rc != pcmk_rc_ok) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not update local CIB to latest schema version");
goto done;
}
scheduler->input = cib_xml_copy;
scheduler->priv->now = crm_time_new(NULL);
cluster_status(scheduler);
/* For recording the tickets that are referenced in PCMK_XE_RSC_TICKET
* constraints but have never been granted yet.
*/
pcmk__unpack_constraints(scheduler);
if (options.ticket_cmd == 'l' || options.ticket_cmd == 'L' || options.ticket_cmd == 'w') {
bool raw = false;
bool details = false;
if (options.ticket_cmd == 'L') {
details = true;
} else if (options.ticket_cmd == 'w') {
raw = true;
}
rc = pcmk__ticket_info(out, scheduler, options.ticket_id, details, raw);
exit_code = pcmk_rc2exitc(rc);
if (rc == ENXIO) {
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"No such ticket '%s'", options.ticket_id);
} else if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not get ticket info: %s", pcmk_rc_str(rc));
}
} else if (options.ticket_cmd == 'q') {
rc = pcmk__ticket_state(out, cib_conn, options.ticket_id);
if (rc != pcmk_rc_ok && rc != pcmk_rc_duplicate_id) {
exit_code = pcmk_rc2exitc(rc);
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not query ticket XML: %s", pcmk_rc_str(rc));
} else {
exit_code = CRM_EX_OK;
}
} else if (options.ticket_cmd == 'c') {
rc = pcmk__ticket_constraints(out, cib_conn, options.ticket_id);
exit_code = pcmk_rc2exitc(rc);
if (rc != pcmk_rc_ok) {
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not show ticket constraints: %s", pcmk_rc_str(rc));
}
} else if (options.ticket_cmd == 'G') {
if (options.ticket_id == NULL) {
exit_code = CRM_EX_NOSUCH;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply ticket ID with -t");
goto done;
}
rc = pcmk__ticket_get_attr(out, scheduler, options.ticket_id,
options.get_attr_name, options.attr_default);
exit_code = pcmk_rc2exitc(rc);
} else if (options.ticket_cmd == 'C') {
if (options.ticket_id == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply ticket ID with -t");
goto done;
}
rc = pcmk__ticket_delete(out, cib_conn, scheduler, options.ticket_id,
options.force);
exit_code = pcmk_rc2exitc(rc);
switch (rc) {
case ENXIO:
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"No such ticket '%s'", options.ticket_id);
break;
case EACCES:
ticket_revoke_warning(options.ticket_id);
break;
case pcmk_rc_ok:
case pcmk_rc_duplicate_id:
break;
default:
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not clean up ticket: %s", pcmk_rc_str(rc));
break;
}
} else if (modified) {
if (options.ticket_id == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply ticket ID with -t");
goto done;
}
if (options.attr_value
&& (pcmk__str_empty(options.attr_name))) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply attribute name with -S for -v %s", options.attr_value);
goto done;
}
if (options.attr_name
&& (pcmk__str_empty(options.attr_value))) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply attribute value with -v for -S %s", options.attr_value);
goto done;
}
if (attr_delete != NULL) {
rc = pcmk__ticket_remove_attr(out, cib_conn, scheduler, options.ticket_id,
attr_delete, options.force);
if (rc == EACCES) {
ticket_revoke_warning(options.ticket_id);
exit_code = CRM_EX_UNSAFE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Ticket modification not allowed without --force");
goto done;
}
} else {
rc = pcmk__ticket_set_attr(out, cib_conn, scheduler, options.ticket_id,
attr_set, options.force);
if (rc == EACCES) {
const char *value = NULL;
value = g_hash_table_lookup(attr_set, PCMK__XA_GRANTED);
if (crm_is_true(value)) {
ticket_grant_warning(options.ticket_id);
} else {
ticket_revoke_warning(options.ticket_id);
}
exit_code = CRM_EX_UNSAFE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Ticket modification not allowed without --force");
goto done;
}
}
exit_code = pcmk_rc2exitc(rc);
if (rc != pcmk_rc_ok && error == NULL) {
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Could not modify ticket: %s", pcmk_rc_str(rc));
}
} else if (options.ticket_cmd == 'S') {
/* Correct usage was handled in the "if (modified)" block above, so
* this is just for reporting usage errors
*/
if (pcmk__str_empty(options.attr_name)) {
// We only get here if ticket_cmd was left as default
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply a command");
goto done;
}
if (options.ticket_id == NULL) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply ticket ID with -t");
goto done;
}
if (pcmk__str_empty(options.attr_value)) {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Must supply value with -v for -S %s", options.attr_name);
goto done;
}
} else {
exit_code = CRM_EX_USAGE;
g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
"Unknown command: %c", options.ticket_cmd);
}
done:
if (attr_set) {
g_hash_table_destroy(attr_set);
}
attr_set = NULL;
if (attr_delete) {
g_list_free_full(attr_delete, free);
}
attr_delete = NULL;
pcmk_free_scheduler(scheduler);
scheduler = NULL;
cib__clean_up_connection(&cib_conn);
g_strfreev(processed_args);
pcmk__free_arg_context(context);
g_free(options.attr_default);
g_free(options.attr_id);
free(options.attr_name);
free(options.attr_value);
free(options.get_attr_name);
g_free(options.set_name);
g_free(options.ticket_id);
g_free(options.xml_file);
pcmk__output_and_clear_error(&error, out);
if (out != NULL) {
out->finish(out, exit_code, true, NULL);
pcmk__output_free(out);
}
pcmk__unregister_formats();
crm_exit(exit_code);
}