blob: 73a55ed5ab33c2c88889f05b7d6e0d67cbf8eeb9 [file] [edit]
#include "tlbmc/host_state/host_state_collector.h"
#include <errno.h>
#include <poll.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <filesystem> // NOLINT
#include <fstream>
#include <iterator>
#include <memory>
#include <string>
#include <system_error> // NOLINT
#include <thread> // NOLINT
#include <utility>
#include "absl/functional/any_invocable.h"
#include "absl/log/log.h"
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/synchronization/mutex.h"
#include "boost/asio/buffer.hpp" // NOLINT
#include "boost/asio/io_context.hpp" // NOLINT
#include "boost/asio/posix/stream_descriptor.hpp" // NOLINT
#include "boost/system/detail/error_code.hpp" // NOLINT
#include "nlohmann/json.hpp"
#include "nlohmann/json.hpp"
#include "power_control.pb.h"
#include "tlbmc/host_state/power_control.h"
#include "tlbmc/scheduler/scheduler.h"
namespace milotic_tlbmc {
namespace {
constexpr char kPowerStateOn[] =
"xyz.openbmc_project.State.Chassis.PowerState.On";
constexpr char kPowerStateOff[] =
"xyz.openbmc_project.State.Chassis.PowerState.Off";
constexpr char kOSStateStandby[] =
"xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.Standby";
constexpr char kOSStateInactive[] =
"xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.Inactive";
} // namespace
absl::StatusOr<std::unique_ptr<PowerControlInotify>>
PowerControlInotify::Create(const PowerControl::Params& params) {
if (params.power_control_type != PowerControl::PowerControlType::kFileBased) {
return absl::InvalidArgumentError(
"PowerControlInotify::Create() called with non-file-based power "
"control type.");
}
if (params.host_power_file_path.empty()) {
return absl::InvalidArgumentError(
"PowerControlInotify::Create() called with empty power state file "
"path.");
}
if (params.os_state_file_path.empty()) {
return absl::InvalidArgumentError(
"PowerControlInotify::Create() called with empty post complete file "
"path.");
}
return absl::WrapUnique(new PowerControlInotify(params.host_power_file_path,
params.os_state_file_path,
params.retry_policy));
}
PowerControlInotify::PowerControlInotify(
const std::filesystem::path& host_power_file_path,
const std::filesystem::path& os_state_file_path, RetryPolicy retry_policy)
: host_power_file_path_(host_power_file_path),
os_state_file_path_(os_state_file_path),
scheduler_(std::make_unique<milotic_tlbmc::TaskScheduler>()),
retry_policy_(retry_policy) {}
PowerControlInotify::~PowerControlInotify() { Stop(); }
HostPowerState PowerControlInotify::ReadPowerState() {
std::ifstream ifs(host_power_file_path_);
if (!ifs.is_open()) {
LOG(WARNING) << "Failed to open file: " << host_power_file_path_;
return HostPowerState::HOST_POWER_STATE_UNKNOWN;
}
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
nlohmann::json data = nlohmann::json::parse(content, /*cb=*/nullptr,
/*allow_exceptions=*/false,
/*ignore_comments=*/true);
if (data.is_discarded()) {
LOG(ERROR) << "Failed to parse JSON from file: " << host_power_file_path_
<< ", content: " << content;
return HostPowerState::HOST_POWER_STATE_UNKNOWN;
}
if (data.contains("PowerState")) {
std::string state_str = data["PowerState"];
LOG(INFO) << "Read PowerState from file: " << state_str;
if (state_str == kPowerStateOn) {
return HostPowerState::HOST_POWER_STATE_ON;
}
if (state_str == kPowerStateOff) {
return HostPowerState::HOST_POWER_STATE_OFF;
}
} else {
LOG(WARNING) << "Host PowerState not found in file: "
<< host_power_file_path_ << ", content: " << data.dump(2);
}
return HostPowerState::HOST_POWER_STATE_UNKNOWN;
}
OSState PowerControlInotify::ReadOSState() {
std::ifstream ifs(os_state_file_path_);
if (!ifs.is_open()) {
LOG(WARNING) << "Failed to open file: " << os_state_file_path_;
return OSState::OS_STATE_UNKNOWN;
}
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
nlohmann::json data = nlohmann::json::parse(content, /*cb=*/nullptr,
/*allow_exceptions=*/false,
/*ignore_comments=*/true);
if (data.is_discarded()) {
LOG(ERROR) << "Failed to parse JSON from file: " << os_state_file_path_
<< ", content: " << content;
return OSState::OS_STATE_UNKNOWN;
}
if (data.contains("OperatingSystemState")) {
std::string state_str = data["OperatingSystemState"];
LOG(INFO) << "Read OSState from file: " << state_str;
if (state_str == kOSStateStandby) {
return OSState::OS_STATE_STANDBY;
}
if (state_str == kOSStateInactive) {
return OSState::OS_STATE_INACTIVE;
}
} else {
LOG(WARNING) << "Host OSState not found in file: " << os_state_file_path_
<< ", content: " << data.dump(2);
}
return OSState::OS_STATE_UNKNOWN;
}
void PowerControlInotify::ProcessPowerStateChange(HostPowerState new_state) {
absl::MutexLock lock(power_state_callback_mutex_);
DLOG(INFO) << "Entering ProcessPowerStateChange: new_state: "
<< HostPowerState_Name(new_state) << " current_power_state: "
<< HostPowerState_Name(current_power_state_);
if (new_state == current_power_state_) {
LOG(INFO) << "Host power state has not changed: "
<< HostPowerState_Name(new_state);
return;
}
if (new_state == HostPowerState::HOST_POWER_STATE_UNKNOWN) {
LOG(WARNING) << "New host power state is unknown, not processing change.";
return;
}
LOG(WARNING) << "Host power state changed from "
<< HostPowerState_Name(current_power_state_) << " to "
<< HostPowerState_Name(new_state);
// To off transition.
if (new_state == HostPowerState::HOST_POWER_STATE_OFF) {
for (const auto& cb : host_power_on_to_off_callbacks_) {
cb(/*setup_run=*/false);
}
} else {
// To on transition.
for (const auto& cb : host_power_off_to_on_callbacks_) {
cb(/*setup_run=*/false);
}
}
current_power_state_ = new_state;
DLOG(INFO) << "Exiting ProcessPowerStateChange: new_state: "
<< HostPowerState_Name(new_state) << " current_power_state: "
<< HostPowerState_Name(current_power_state_);
}
void PowerControlInotify::ProcessOSStateChange(OSState new_state) {
absl::MutexLock lock(os_state_callback_mutex_);
if (new_state == current_os_state_) {
LOG(INFO) << "Host OS state has not changed: " << OSState_Name(new_state);
return;
}
if (new_state == OSState::OS_STATE_UNKNOWN) {
LOG(WARNING) << "New host OS state is unknown, not processing change.";
return;
}
LOG(WARNING) << "Host OS state changed from "
<< OSState_Name(current_os_state_) << " to "
<< OSState_Name(new_state);
// To standby transition.
if (new_state == OSState::OS_STATE_STANDBY) {
for (const auto& cb : host_os_inactive_to_standby_callbacks_) {
cb(/*setup_run=*/false);
}
} else {
// To inactive transition.
for (const auto& cb : host_os_standby_to_inactive_callbacks_) {
cb(/*setup_run=*/false);
}
}
current_os_state_ = new_state;
}
void PowerControlInotify::HostPowerReadHandler(
const boost::system::error_code& ec, std::size_t length) {
DLOG(INFO) << "InotifyReadHandler: ec: " << ec.message()
<< " length: " << length;
if (ec) {
if (ec == boost::asio::error::operation_aborted) {
DLOG(INFO) << "Inotify read aborted";
return;
}
LOG(ERROR) << "Host power inotify read failed: " << ec.message();
return;
}
uint32_t i = 0;
while (i < length) {
struct inotify_event* event =
reinterpret_cast<struct inotify_event*>(&host_power_inotify_buf_[i]);
if ((event->mask & IN_CLOSE_WRITE) != 0u) {
ProcessPowerStateChange(ReadPowerState());
}
i += sizeof(struct inotify_event) + event->len;
}
ScheduleInotifyRead(
*host_power_inotify_stream_, host_power_inotify_buf_,
[this](const boost::system::error_code& ec, std::size_t length) {
HostPowerReadHandler(ec, length);
});
}
void PowerControlInotify::OsStateReadHandler(
const boost::system::error_code& ec, std::size_t length) {
DLOG(INFO) << "OsStateReadHandler: ec: " << ec.message()
<< " length: " << length;
if (ec) {
if (ec == boost::asio::error::operation_aborted) {
return;
}
LOG(ERROR) << "OsState inotify read failed: " << ec.message();
return;
}
uint32_t i = 0;
while (i < length) {
struct inotify_event* event =
reinterpret_cast<struct inotify_event*>(&os_state_inotify_buf_[i]);
if ((event->mask & IN_CLOSE_WRITE) != 0u) {
ProcessOSStateChange(ReadOSState());
}
i += sizeof(struct inotify_event) + event->len;
}
ScheduleInotifyRead(
*os_state_inotify_stream_, os_state_inotify_buf_,
[this](const boost::system::error_code& ec, std::size_t length) {
OsStateReadHandler(ec, length);
});
}
void PowerControlInotify::ScheduleInotifyRead(
boost::asio::posix::stream_descriptor& stream,
std::array<char, kInotifyBufLen>& buf,
absl::AnyInvocable<void(const boost::system::error_code&, std::size_t)>
handler) {
stream.async_read_some(boost::asio::buffer(buf), std::move(handler));
}
absl::Status PowerControlInotify::StartInternal() {
std::error_code ec;
if (!std::filesystem::exists(host_power_file_path_, ec)) {
return absl::NotFoundError(absl::StrCat("File to monitor does not exist: ",
host_power_file_path_.string()));
}
if (!std::filesystem::exists(os_state_file_path_, ec)) {
return absl::NotFoundError(absl::StrCat("OS state file does not exist: ",
os_state_file_path_.string()));
}
host_power_inotify_fd_ = inotify_init1(IN_NONBLOCK);
if (host_power_inotify_fd_ < 0) {
return absl::InternalError(
absl::StrCat("inotify_init1 failed: ", strerror(errno)));
}
host_power_watch_fd_ = inotify_add_watch(
host_power_inotify_fd_, host_power_file_path_.c_str(), IN_CLOSE_WRITE);
if (host_power_watch_fd_ < 0) {
close(host_power_inotify_fd_);
host_power_inotify_fd_ = -1;
return absl::InternalError(absl::StrCat("inotify_add_watch failed for ",
host_power_file_path_.string(),
": ", strerror(errno)));
}
host_power_inotify_stream_ =
std::make_unique<boost::asio::posix::stream_descriptor>(io_context_);
boost::system::error_code asio_ec;
// The assign method returns void, and errors are reported via the 'asio_ec'
// parameter.
// NOLINTNEXTLINE(bugprone-unused-return-value)
(void)host_power_inotify_stream_->assign(host_power_inotify_fd_, asio_ec);
if (asio_ec) {
inotify_rm_watch(host_power_inotify_fd_, host_power_watch_fd_);
close(host_power_inotify_fd_);
host_power_inotify_fd_ = -1;
host_power_watch_fd_ = -1;
return absl::InternalError(absl::StrCat(
"Failed to assign fd to asio stream: ", asio_ec.message()));
}
ScheduleInotifyRead(
*host_power_inotify_stream_, host_power_inotify_buf_,
[this](const boost::system::error_code& ec, std::size_t length) {
HostPowerReadHandler(ec, length);
});
os_state_inotify_fd_ = inotify_init1(IN_NONBLOCK);
if (os_state_inotify_fd_ < 0) {
host_power_inotify_stream_.reset();
inotify_rm_watch(host_power_inotify_fd_, host_power_watch_fd_);
close(host_power_inotify_fd_);
host_power_inotify_fd_ = -1;
host_power_watch_fd_ = -1;
return absl::InternalError(absl::StrCat(
"inotify_init1 failed for post complete: ", strerror(errno)));
}
os_state_watch_fd_ = inotify_add_watch(
os_state_inotify_fd_, os_state_file_path_.c_str(), IN_CLOSE_WRITE);
if (os_state_watch_fd_ < 0) {
host_power_inotify_stream_.reset();
inotify_rm_watch(host_power_inotify_fd_, host_power_watch_fd_);
close(host_power_inotify_fd_);
host_power_inotify_fd_ = -1;
host_power_watch_fd_ = -1;
close(os_state_inotify_fd_);
os_state_inotify_fd_ = -1;
return absl::InternalError(absl::StrCat(
"inotify_add_watch failed for os state: ", os_state_file_path_.string(),
": ", strerror(errno)));
}
os_state_inotify_stream_ =
std::make_unique<boost::asio::posix::stream_descriptor>(io_context_);
// The assign method returns void, and errors are reported via the 'asio_ec'
// parameter.
// NOLINTNEXTLINE(bugprone-unused-return-value)
(void)os_state_inotify_stream_->assign(os_state_inotify_fd_, asio_ec);
if (asio_ec) {
// Cleanup if assign failed.
inotify_rm_watch(os_state_inotify_fd_, os_state_watch_fd_);
close(os_state_inotify_fd_);
os_state_inotify_fd_ = -1;
os_state_watch_fd_ = -1;
// Cleanup host power inotify stream.
host_power_inotify_stream_.reset();
inotify_rm_watch(host_power_inotify_fd_, host_power_watch_fd_);
close(host_power_inotify_fd_);
host_power_inotify_fd_ = -1;
host_power_watch_fd_ = -1;
return absl::InternalError(
absl::StrCat("Failed to assign fd to asio stream for post complete: ",
asio_ec.message()));
}
ScheduleInotifyRead(
*os_state_inotify_stream_, os_state_inotify_buf_,
[this](const boost::system::error_code& ec, std::size_t length) {
OsStateReadHandler(ec, length);
});
io_thread_ = std::thread([this] { io_context_.run(); });
// After callbacks are registered, we need to check the current power state
// and call the callbacks if needed. Holding callback_mutex_ to avoid any
// race condition with the callbacks and guarantee no event is missed.
{
absl::MutexLock lock(power_state_callback_mutex_);
current_power_state_ = ReadPowerState();
if (current_power_state_ == HostPowerState::HOST_POWER_STATE_UNKNOWN) {
LOG(WARNING) << "Current host power state is unknown (read partial "
"content), not calling callbacks.";
} else if (current_power_state_ == HostPowerState::HOST_POWER_STATE_ON) {
for (const auto& cb : host_power_off_to_on_callbacks_) {
cb(/*setup_run=*/true);
}
} else { // HostPowerState::HOST_POWER_STATE_OFF
for (const auto& cb : host_power_on_to_off_callbacks_) {
cb(/*setup_run=*/true);
}
}
}
{
absl::MutexLock lock(os_state_callback_mutex_);
current_os_state_ = ReadOSState();
if (current_os_state_ == OSState::OS_STATE_UNKNOWN) {
LOG(WARNING) << "Current OS state is unknown (read partial content), "
"not calling callbacks.";
} else if (current_os_state_ == OSState::OS_STATE_STANDBY) {
for (const auto& cb : host_os_inactive_to_standby_callbacks_) {
cb(/*setup_run=*/true);
}
} else { // OSState::OS_STATE_INACTIVE
for (const auto& cb : host_os_standby_to_inactive_callbacks_) {
cb(/*setup_run=*/true);
}
}
}
return absl::OkStatus();
}
void PowerControlInotify::Stop() {
absl::MutexLock lock(start_stop_mutex_);
if (stop_state_ == StopState::kStopRequested) {
return;
}
stop_state_ = StopState::kStopRequested;
io_context_.stop();
scheduler_->Stop();
// If the io_thread_ is not joinable, it means that the io_context_ is not
// running or already joined. This is to avoid crashing the program when
// Stop() is called multiple times.
if (!io_thread_.joinable()) {
return;
}
io_thread_.join();
if (host_power_watch_fd_ >= 0 && host_power_inotify_fd_ >= 0) {
if (inotify_rm_watch(host_power_inotify_fd_, host_power_watch_fd_) < 0) {
LOG(ERROR) << "Failed to remove inotify watch: " << strerror(errno);
}
host_power_watch_fd_ = -1;
}
if (os_state_watch_fd_ >= 0 && os_state_inotify_fd_ >= 0) {
if (inotify_rm_watch(os_state_inotify_fd_, os_state_watch_fd_) < 0) {
LOG(ERROR) << "Failed to remove os state inotify watch: "
<< strerror(errno);
}
os_state_watch_fd_ = -1;
}
host_power_inotify_stream_.reset();
host_power_inotify_fd_ = -1;
os_state_inotify_stream_.reset();
os_state_inotify_fd_ = -1;
}
void PowerControlInotify::RegisterHostPowerOnToOffCallback(
absl::AnyInvocable<void(bool) const> callback) {
absl::MutexLock lock(power_state_callback_mutex_);
host_power_on_to_off_callbacks_.push_back(std::move(callback));
}
void PowerControlInotify::RegisterHostPowerOffToOnCallback(
absl::AnyInvocable<void(bool) const> callback) {
absl::MutexLock lock(power_state_callback_mutex_);
host_power_off_to_on_callbacks_.push_back(std::move(callback));
}
void PowerControlInotify::RegisterHostOSInactiveToStandbyCallback(
absl::AnyInvocable<void(bool) const> callback) {
absl::MutexLock lock(os_state_callback_mutex_);
host_os_inactive_to_standby_callbacks_.push_back(std::move(callback));
}
void PowerControlInotify::RegisterHostOSStandbyToInactiveCallback(
absl::AnyInvocable<void(bool) const> callback) {
absl::MutexLock lock(os_state_callback_mutex_);
host_os_standby_to_inactive_callbacks_.push_back(std::move(callback));
}
void PowerControlInotify::Start() {
DLOG(INFO) << "Deterministic BMC: PowerControlInotify::Start() "
"called with power file path: "
<< host_power_file_path_
<< " and os state file path: " << os_state_file_path_;
absl::MutexLock lock(start_stop_mutex_);
// This is to avoid any in queue task starting the collector again if Stop()
// is called.
if (stop_state_ == StopState::kStopRequested) {
return;
}
absl::Status status = StartInternal();
if (status.ok()) {
// StartInternal() succeeded, no need to retry.
retry_task_id_ = -1;
return;
}
if (absl::IsNotFound(status) &&
start_retry_count_ < retry_policy_.max_retries) {
start_retry_count_++;
LOG(WARNING)
<< "PowerControlInotify::StartInternal() failed with NotFoundError: "
<< status << ". retrying in " << retry_policy_.retry_interval
<< " for attempt " << start_retry_count_;
retry_task_id_ = scheduler_->ScheduleOneShotAsync(
[this](auto on_done) {
Start();
on_done();
},
retry_policy_.retry_interval);
} else {
LOG(ERROR) << "PowerControlInotify::StartInternal() failed after "
<< start_retry_count_
<< " retries or with non-retryable error: " << status;
retry_task_id_ = -1;
}
}
nlohmann::json PowerControlInotify::ToJson() {
nlohmann::json json;
absl::MutexLock power_state_lock(power_state_callback_mutex_);
absl::MutexLock os_state_lock(os_state_callback_mutex_);
json["PowerState"] = HostPowerState_Name(current_power_state_);
json["OSState"] = OSState_Name(current_os_state_);
return json;
}
HostState PowerControlInotify::GetHostState() {
absl::MutexLock power_state_lock(power_state_callback_mutex_);
absl::MutexLock os_state_lock(os_state_callback_mutex_);
HostState host_state;
switch (current_power_state_) {
case HostPowerState::HOST_POWER_STATE_OFF:
host_state.set_power_state(PowerState::POWER_STATE_OFF);
break;
case HostPowerState::HOST_POWER_STATE_ON:
host_state.set_power_state(PowerState::POWER_STATE_ON);
break;
default:
host_state.set_power_state(PowerState::POWER_STATE_UNSPECIFIED);
break;
}
host_state.set_os_state(current_os_state_);
return host_state;
}
} // namespace milotic_tlbmc