| #include "bmc/register_actions_bmc.h" |
| |
| #include <sys/stat.h> |
| |
| #include <cstdint> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "action_context.h" |
| #include "bmc/redfish.h" |
| #include "daemon_context.h" |
| #include "disruption_manager.h" |
| #include "safepower_agent.pb.h" |
| #include "safepower_agent_config.pb.h" |
| #include "absl/functional/any_invocable.h" |
| #include "absl/functional/bind_front.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| |
| // NOLINTBEGIN(readability/boost) |
| #include "absl/time/time.h" |
| #include "boost/asio/ip/tcp.hpp" |
| #include "boost/beast/core.hpp" |
| #include "boost/beast/http.hpp" |
| // NOLINTEND(readability/boost) |
| |
| #include "nlohmann/json.hpp" |
| #include "nlohmann/json_fwd.hpp" |
| |
| namespace safepower_agent { |
| |
| using safepower_agent_config::SafePowerAgentConfig; |
| |
| // runs after the action is called, parses the response to a status |
| inline absl::Status GenericRedfishParser(absl::StatusOr<nlohmann::json> js) { |
| if (!js.ok()) { |
| LOG(ERROR) << "BMC connection failed: " << js.status(); |
| return js.status(); |
| } |
| auto extended_info = (*js)["@Message.ExtendedInfo"]; |
| if (extended_info.size() != 1) { |
| LOG(ERROR) << "redfish response has an abnormal number of message : " |
| << *js; |
| } |
| auto info = extended_info[0]; |
| auto message_id = info.find("MessageId"); |
| if (message_id == info.end()) { |
| LOG(ERROR) << "Failed to parse bmc's response to JSON: " << *js; |
| return absl::InvalidArgumentError("Failed to parse bmc's response to JSON"); |
| } |
| std::string status = *message_id; |
| std::vector<std::string> v = absl::StrSplit(status, '.'); |
| if (v.size() >= 4 && v[4] == "Success") { |
| return absl::OkStatus(); |
| } else { |
| LOG(ERROR) << "Failed to power off :" << *message_id; |
| return absl::NotFoundError("Failed to power off :" + message_id->dump()); |
| } |
| } |
| |
| struct GenericRedfishPowerOperationConfig { |
| std::string uri; |
| nlohmann::json body; |
| std::string ip; |
| uint16_t port; |
| absl::Duration call_timeout = absl::Seconds(10); |
| }; |
| |
| // runs when then action is called, perform the action |
| inline void GenericRedfishPowerOperation( |
| const GenericRedfishPowerOperationConfig& config, |
| const safepower_agent_proto::Action&, |
| absl::AnyInvocable<void(absl::Status) &&> cb) { |
| LOG(INFO) << "Redfish action " << config.ip << ":" << config.port << " " |
| << config.uri << " " << config.body; |
| Redfish::Post( |
| config.uri, |
| [callback = |
| std::move(cb)](absl::StatusOr<nlohmann::json> result) mutable { |
| std::move(callback)(GenericRedfishParser(std::move(result))); |
| }, |
| config.body, config.ip, config.port); |
| } |
| |
| // Callable that runs after all disruption callbacks are completed, then waits |
| // for the disruption to happen or time out. This is a callable because the |
| // callback handle needs to be owned by the callback. |
| class DisruptiveRedfishPowerOperation { |
| public: |
| explicit DisruptiveRedfishPowerOperation( |
| GenericRedfishPowerOperationConfig config, absl::Duration timeout) |
| : config_(std::move(config)), timeout_(timeout) {} |
| |
| DisruptiveRedfishPowerOperation(DisruptiveRedfishPowerOperation&&) = default; |
| DisruptiveRedfishPowerOperation& operator=( |
| DisruptiveRedfishPowerOperation&&) = default; |
| |
| void operator()(const safepower_agent_proto::Action& /*action*/, |
| absl::AnyInvocable<void(absl::Status) &&> cb) { |
| callback_ = std::move(cb); |
| auto& disruption_manager = DaemonContext::Get().disruption_manager(); |
| // Run after all the other disruption callbacks are completed. |
| run_handle_ = disruption_manager.OnDisruptionStart( |
| absl::bind_front(&DisruptiveRedfishPowerOperation::Run, this)); |
| absl::Status status = disruption_manager.ExpectDisruptionIn(timeout_); |
| if (!status.ok()) { |
| LOG(DFATAL) << "Failed to expect disruption: " << status; |
| run_handle_ = {}; // Cancel the disruption start callback. |
| std::move(callback_)(status); |
| return; |
| } |
| } |
| |
| private: |
| void Run() { |
| GenericRedfishPowerOperation(config_, {}, [this](absl::Status status) { |
| if (!status.ok()) { |
| std::move(callback_)(status); |
| return; |
| } |
| // TODO: b/416304657 - Monitor progress of power |
| // operations and end early if it won't work. For now, we just let the |
| // disruption happen or expire without calling the callback. |
| }); |
| } |
| |
| GenericRedfishPowerOperationConfig config_; |
| absl::Duration timeout_; |
| absl::AnyInvocable<void(absl::Status) &&> callback_; |
| DisruptionManager::CallbackHandle run_handle_; |
| }; |
| |
| absl::Status RegisterNoOpAction(ActionContextManager& action_context_manager) { |
| return action_context_manager.RegisterAction( |
| safepower_agent_proto::Action::default_instance(), |
| [](const safepower_agent_proto::Action& /*action*/, |
| absl::AnyInvocable<void(absl::Status) &&> cb) { |
| LOG(WARNING) << "Running No-op action"; |
| std::move(cb)(absl::OkStatus()); |
| }); |
| } |
| |
| // runs at startup to register all actions from the config |
| absl::Status RegisterActionFromConfig( |
| ActionContextManager* action_context_manager, |
| const SafePowerAgentConfig& config) { |
| if (absl::Status status = RegisterNoOpAction(*action_context_manager); |
| !status.ok()) { |
| LOG(ERROR) << "Unable to register no-op action :" << status; |
| } |
| for (const auto& action_config : config.action_configs()) { |
| safepower_agent_proto::Action action_to_register = action_config.action(); |
| // The config will map actions to redfish (uris and body) |
| // or dbus (path, method, interface, value) |
| GenericRedfishPowerOperationConfig redfish_config; |
| redfish_config.body = nlohmann::json::parse( |
| action_config.redfish().json_body(), /* cb */ nullptr, |
| /* allow_exceptions */ false); |
| if (redfish_config.body.is_discarded()) { |
| LOG(ERROR) << "Failed to parse json body: " |
| << action_config.redfish().json_body(); |
| return absl::InvalidArgumentError("Failed to parse json body: " + |
| action_config.redfish().json_body()); |
| } |
| redfish_config.uri = action_config.redfish().uri(); |
| if (action_config.redfish().port() > 65535) { |
| LOG(ERROR) << "Port number is invalid: " |
| << action_config.redfish().port(); |
| return absl::InvalidArgumentError(absl::StrCat( |
| "Port number is invalid: ", action_config.redfish().port())); |
| } |
| redfish_config.port = action_config.redfish().port(); |
| redfish_config.ip = action_config.redfish().ip(); |
| LOG(INFO) << "registering " << action_config.action_name() << " " |
| << redfish_config.ip << ":" << redfish_config.port << " " |
| << redfish_config.uri << " " << redfish_config.body; |
| if (action_config.redfish().call_timeout_seconds() > 0) { |
| redfish_config.call_timeout = |
| absl::Seconds(action_config.redfish().call_timeout_seconds()); |
| } |
| |
| absl::Status action_status; |
| if (action_config.disruption_timeout_seconds() > 0) { |
| action_status = action_context_manager->RegisterAction( |
| action_to_register, |
| DisruptiveRedfishPowerOperation( |
| std::move(redfish_config), |
| absl::Seconds(action_config.disruption_timeout_seconds()))); |
| } else { |
| action_status = action_context_manager->RegisterAction( |
| action_to_register, absl::bind_front(GenericRedfishPowerOperation, |
| std::move(redfish_config))); |
| } |
| |
| if (!action_status.ok()) { |
| LOG(ERROR) << "Unable to register actions :" << action_status; |
| return action_status; |
| } |
| } |
| LOG(INFO) << "All actions registered"; |
| return absl::OkStatus(); |
| } |
| } // namespace safepower_agent |