Skip to content
1 change: 1 addition & 0 deletions pcs/common/reports/codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@
UNABLE_TO_GET_CLUSTER_KNOWN_HOSTS = M("UNABLE_TO_GET_CLUSTER_KNOWN_HOSTS")
UNABLE_TO_GET_SBD_CONFIG = M("UNABLE_TO_GET_SBD_CONFIG")
UNABLE_TO_GET_SBD_STATUS = M("UNABLE_TO_GET_SBD_STATUS")
UNABLE_TO_SET_SBD_CONFIG = M("UNABLE_TO_SET_SBD_CONFIG")
UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE = M(
"UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE"
)
Expand Down
18 changes: 18 additions & 0 deletions pcs/common/reports/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4318,6 +4318,24 @@ def message(self) -> str:
)


@dataclass(frozen=True)
class UnableToSetSbdConfig(ReportItemMessage):
"""
Unable to set SBD configuration

reason -- reason of failure
"""

reason: str
_code = codes.UNABLE_TO_SET_SBD_CONFIG

@property
def message(self) -> str:
return "Unable to set SBD configuration{reason}".format(
reason=format_optional(self.reason, ": {}"),
)


@dataclass(frozen=True)
class SbdDeviceInitializationStarted(ReportItemMessage):
"""
Expand Down
25 changes: 25 additions & 0 deletions pcs/daemon/app/api_v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,28 @@ async def _handle_request(self) -> None:
self.write("Succeeded")


class GetSbdConfigHandler(_BaseApiV0Handler):
async def _handle_request(self) -> None:
result = await self._run_library_command(
"sbd.get_node_sbd_config_text", {}
)
if not result.success:
raise self._error(reports_to_str(result.reports))
self.write(result.result)


class SetSbdConfigHandler(_BaseApiV0Handler):
async def _handle_request(self) -> None:
self._check_required_params({"config"})
result = await self._run_library_command(
"sbd.set_node_sbd_config_text",
dict(config=self.get_argument("config")),
)
if not result.success:
raise self._error(reports_to_str(result.reports))
self.write("SBD configuration saved.")


def get_routes(
api_auth_provider_factory: ApiAuthProviderFactoryInterface,
scheduler: Scheduler,
Expand Down Expand Up @@ -741,4 +763,7 @@ def r(url: str) -> str:
(r("check_host"), CheckHostHandler, params),
# cluster config
(r("set_corosync_conf"), SetCorosyncConf, params),
# sbd
(r("get_sbd_config"), GetSbdConfigHandler, params),
(r("set_sbd_config"), SetSbdConfigHandler, params),
]
12 changes: 12 additions & 0 deletions pcs/daemon/async_tasks/worker/command_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,14 @@ class _Cmd:
cmd=sbd.enable_sbd,
required_permission=p.WRITE,
),
"sbd.get_node_sbd_config_text": _Cmd(
cmd=sbd.get_node_sbd_config_text,
required_permission=p.READ,
),
"sbd.set_node_sbd_config_text": _Cmd(
cmd=sbd.set_node_sbd_config_text,
required_permission=p.WRITE,
),
"scsi.unfence_node": _Cmd(
cmd=scsi.unfence_node,
required_permission=p.WRITE,
Expand Down Expand Up @@ -550,6 +558,10 @@ class _Cmd:
"resource_agent.list_agents_for_standard_and_provider",
"resource_agent.list_ocf_providers",
"resource_agent.list_standards",
# The sbd URLs are ready to be exposed in APIv2, just waiting for all the
# other URLs to get moved to APIv2
"sbd.get_node_sbd_config_text",
"sbd.set_node_sbd_config_text",
# There is a lot of url handlers managing cluster services and they should
# be consolidated eventually. These are considered temporary implementation
# for the purpose of removing ruby code. Therefore, they are not exposed in
Expand Down
24 changes: 24 additions & 0 deletions pcs/lib/commands/sbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,30 @@ def get_local_sbd_config(lib_env: LibraryEnvironment) -> dict[str, str]:
return environment_file_to_dict(sbd.get_local_sbd_config())


def get_node_sbd_config_text(lib_env: LibraryEnvironment) -> str:
"""
Returns local SBD configuration as a raw string.
"""
try:
return sbd.get_local_sbd_config()
except LibraryError as e:
lib_env.report_processor.report_list(list(e.args))
raise LibraryError() from e


def set_node_sbd_config_text(lib_env: LibraryEnvironment, config: str) -> None:
"""
Set SBD configuration on local node from a config string.

config -- SBD configuration as string
"""
try:
sbd.set_local_sbd_config(config)
except LibraryError as e:
lib_env.report_processor.report_list(list(e.args))
raise LibraryError() from e


def initialize_block_devices(
lib_env: LibraryEnvironment,
device_list: StringSequence,
Expand Down
23 changes: 23 additions & 0 deletions pcs/lib/sbd.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fcntl
import re
from os import path
from typing import (
Expand Down Expand Up @@ -252,6 +253,7 @@ def get_local_sbd_config() -> str:
"""
try:
with open(settings.sbd_config, "r") as sbd_cfg:
fcntl.flock(sbd_cfg.fileno(), fcntl.LOCK_SH)
return sbd_cfg.read()
except EnvironmentError as e:
raise LibraryError(
Expand All @@ -261,6 +263,27 @@ def get_local_sbd_config() -> str:
) from e


def set_local_sbd_config(config: str) -> None:
"""
Write SBD configuration to local SBD config file. Raise LibraryError on
any failure.

config -- SBD configuration as string
"""
try:
with open(settings.sbd_config, "w") as sbd_cfg:
# the lock is released when the file gets closed on leaving the
# with statement
fcntl.flock(sbd_cfg.fileno(), fcntl.LOCK_EX)
sbd_cfg.write(config)
except EnvironmentError as e:
raise LibraryError(
reports.ReportItem.error(
reports.messages.UnableToSetSbdConfig(str(e))
)
) from e


def is_sbd_enabled(service_manager: ServiceManagerInterface) -> bool:
"""
Check if SBD service is enabled in local system.
Expand Down
2 changes: 2 additions & 0 deletions pcs_test/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ EXTRA_DIST = \
tools/command_env/calls.py \
tools/command_env/config_corosync_conf.py \
tools/command_env/config_env.py \
tools/command_env/config_fcntl.py \
tools/command_env/config_fs.py \
tools/command_env/config_http_booth.py \
tools/command_env/config_http_corosync.py \
Expand All @@ -579,6 +580,7 @@ EXTRA_DIST = \
tools/command_env/config_runner_scsi.py \
tools/command_env/config_services.py \
tools/command_env/__init__.py \
tools/command_env/mock_fcntl.py \
tools/command_env/mock_fs.py \
tools/command_env/mock_get_local_corosync_conf.py \
tools/command_env/mock_node_communicator.py \
Expand Down
14 changes: 14 additions & 0 deletions pcs_test/tier0/common/reports/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3104,6 +3104,20 @@ def test_all(self):
)


class UnableToSetSbdConfig(NameBuildTest):
def test_no_reason(self):
self.assert_message_from_report(
"Unable to set SBD configuration",
reports.UnableToSetSbdConfig(""),
)

def test_all(self):
self.assert_message_from_report(
"Unable to set SBD configuration: reason",
reports.UnableToSetSbdConfig("reason"),
)


class SbdDeviceInitializationStarted(NameBuildTest):
def test_more_devices(self):
self.assert_message_from_report(
Expand Down
49 changes: 49 additions & 0 deletions pcs_test/tier0/daemon/app/test_api_v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -1746,3 +1746,52 @@ def test_empty_corosync_conf(self):
)
self.assertEqual(response.code, 400)
self.mock_run_library_command.assert_not_called()


class GetSbdConfigHandler(ApiV0HandlerTest):
url = "/remote/get_sbd_config"
body_data = "sbd config text data\n"

def test_success(self):
self.mock_run_library_command.return_value = self.result_success(
self.body_data
)
response = self.fetch(self.url)
self.assert_body(response.body, self.body_data)
self.assertEqual(response.code, 200)
self.mock_run_library_command.assert_called_once_with(
"sbd.get_node_sbd_config_text", {}
)

def test_failure(self):
self.assert_error_with_report(self.url)
self.mock_run_library_command.assert_called_once_with(
"sbd.get_node_sbd_config_text", {}
)


class SetSbdConfigHandler(ApiV0HandlerTest):
url = "/remote/set_sbd_config"
body_data = {"config": "sbd config text data\n"}
command_data = {"config": body_data["config"].strip()}

def test_success(self):
self.mock_run_library_command.return_value = self.result_success()
response = self.fetch(self.url, body=urlencode(self.body_data))
self.assert_body(response.body, "SBD configuration saved.")
self.assertEqual(response.code, 200)
self.mock_run_library_command.assert_called_once_with(
"sbd.set_node_sbd_config_text", self.command_data
)

def test_missing_params(self):
response = self.fetch(self.url)
self.assert_body(response.body, "Required parameters missing: 'config'")
self.assertEqual(response.code, 400)
self.mock_run_library_command.assert_not_called()

def test_failure(self):
self.assert_error_with_report(self.url, body=urlencode(self.body_data))
self.mock_run_library_command.assert_called_once_with(
"sbd.set_node_sbd_config_text", self.command_data
)
35 changes: 29 additions & 6 deletions pcs_test/tier0/lib/commands/cluster/test_add_nodes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pylint: disable=too-many-lines
# pylint: disable=no-member
import base64
import fcntl
import json
import os.path
from functools import partial
Expand Down Expand Up @@ -222,16 +223,23 @@ def atb_needed(self, node_labels):

def read_sbd_config(self, config_content="", name_suffix=""):
local_prefix = "local.read_sbd_config."
mock_file = fixture.get_mock_file(read_data=config_content)
(
self.config.fs.exists(
settings.sbd_config,
return_value=True,
name=f"{local_prefix}fs.exists.sbd_config{name_suffix}",
).fs.open(
)
.fs.open(
settings.sbd_config,
return_value=mock.mock_open(read_data=config_content)(),
return_value=mock_file,
name=f"{local_prefix}fs.open.sbd_config_read{name_suffix}",
)
.fcntl.flock(
mock_file,
fcntl.LOCK_SH,
name=f"{local_prefix}fcntl.flock.sbd_config{name_suffix}",
)
)

def check_sbd(self, node_labels, with_devices=True):
Expand Down Expand Up @@ -278,12 +286,18 @@ def disable_sbd(self, node_labels):

def setup_sbd(self, local_config, config_generator, node_labels):
local_prefix = "local.setup_sbd."
mock_file = fixture.get_mock_file(read_data=local_config)
(
self.config.fs.open(
settings.sbd_config,
return_value=mock.mock_open(read_data=local_config)(),
return_value=mock_file,
name=f"{local_prefix}fs.open.sbd_config",
)
.fcntl.flock(
mock_file,
fcntl.LOCK_SH,
name=f"{local_prefix}fcntl.flock.sbd_config",
)
.http.sbd.set_sbd_config(
config_generator=config_generator,
node_labels=node_labels,
Expand Down Expand Up @@ -3534,12 +3548,16 @@ def _add_nodes_with_lib_error(self, report_list=None):
)

def test_enable_communication_failure(self):
sbd_mock_file = fixture.get_mock_file(read_data=self.sbd_config)
(
self.config.fs.open(
settings.sbd_config,
return_value=mock.mock_open(read_data=self.sbd_config)(),
return_value=sbd_mock_file,
name="fs.open.sbd_config",
)
.fcntl.flock(
sbd_mock_file, fcntl.LOCK_SH, name="fcntl.flock.sbd_config"
)
.http.sbd.set_sbd_config(
config_generator=lambda node: sbd_config_generator(
node, with_devices=False
Expand Down Expand Up @@ -3601,12 +3619,17 @@ def test_enable_communication_failure(self):
)

def test_send_config_communication_failure(self):
sbd_mock_file = fixture.get_mock_file(read_data=self.sbd_config)
(
self.config.fs.open(
settings.sbd_config,
return_value=mock.mock_open(read_data=self.sbd_config)(),
return_value=sbd_mock_file,
name="fs.open.sbd_config",
).http.sbd.set_sbd_config(
)
.fcntl.flock(
sbd_mock_file, fcntl.LOCK_SH, name="fcntl.flock.sbd_config"
)
.http.sbd.set_sbd_config(
communication_list=[
dict(
label=node,
Expand Down
Loading
Loading