| #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 |