blob: cd15e382b8f3dbb4ba4e33195808fb340fe576ce [file]
#include "tlbmc/redfish/routes/sensor.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/flags/flag.h"
#include "absl/functional/bind_front.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "bmcweb_config.h"
#include "http_request.hpp"
#include "async_resp.hpp"
#include <nlohmann/json.hpp>
#include "json_utils.h"
#include "tlbmc/central_config/config.h"
#include "entity_common_config.pb.h"
#include "reading_range_config.pb.h"
#include "threshold_config.pb.h"
#include "topology_config.pb.h"
#include "tlbmc/redfish/app.h"
#include "tlbmc/redfish/request.h"
#include "tlbmc/redfish/response.h"
#include "tlbmc/redfish/url.h"
#include "resource.pb.h"
#include "sensor.pb.h"
#include "tlbmc/sensors/sensor.h"
#include "tlbmc/store/store.h"
#include "smart_router.h"
ABSL_FLAG(bool, enable_tlbmc_sensor_override, enableTlbmcSensorOverride,
"Enable creation of tlBMC sensor overrides. Defaults to true when "
"compiled with tlbmc-sensor-override and false otherwise.");
namespace milotic_tlbmc::sensor {
namespace {
constexpr std::string_view kHealthOk = "OK";
constexpr std::string_view kHealthWarning = "Warning";
constexpr std::string_view kHealthCritical = "Critical";
void HandleSensorCollection(const RedfishApp& app, const RedfishRequest& req,
RedfishResponse& resp,
const std::string& chassis_id) {
std::string sensor_collection_url =
absl::Substitute("/redfish/v1/Chassis/$0/Sensors", chassis_id);
resp.SetKeyInJsonBody("/@odata.id", sensor_collection_url);
resp.SetKeyInJsonBody("/@odata.type", "#SensorCollection.SensorCollection");
resp.SetKeyInJsonBody("/Name", "Sensors");
resp.SetKeyInJsonBody("/Description",
"Collection of Sensors for this Chassis");
const Store& store = *app.GetStore();
std::vector<std::string> sensor_keys =
store.GetAllSensorKeysByConfigKey(chassis_id);
std::sort(sensor_keys.begin(), sensor_keys.end());
nlohmann::json::array_t members;
for (const auto& key : sensor_keys) {
std::shared_ptr<const Sensor> sensor_ptr =
store.GetSensorByConfigKeyAndSensorKey(chassis_id, key);
if (sensor_ptr->GetSensorAttributesStatic().redfish_hidden()) {
continue;
}
nlohmann::json::object_t member;
member["@odata.id"] = absl::StrCat(sensor_collection_url, "/", key);
members.push_back(std::move(member));
}
resp.SetKeyInJsonBody("/Members@odata.count", members.size());
resp.SetKeyInJsonBody("/Members", members);
}
void HandleSensorCollectionAfterGbmcweb(
const RedfishApp& app, const ::crow::Request& request,
const std::shared_ptr<bmcweb::AsyncResp>& async_resp,
const std::string& chassis_id) {
nlohmann::json& json = async_resp->res.jsonValue;
nlohmann::json::array_t* members =
milotic::authz::GetValueAsMutableArray(json, "Members");
if (members == nullptr) {
return;
}
std::size_t members_count = members->size();
const Store& store = *app.GetStore();
std::vector<std::string> sensor_keys =
store.GetAllSensorKeysByConfigKey(chassis_id);
int valid_sensors_count = 0;
std::string sensor_collection_url =
absl::Substitute("/redfish/v1/Chassis/$0/Sensors", chassis_id);
for (const auto& key : sensor_keys) {
std::shared_ptr<const Sensor> sensor_ptr =
store.GetSensorByConfigKeyAndSensorKey(chassis_id, key);
if (sensor_ptr == nullptr) {
continue;
}
if (sensor_ptr->GetSensorAttributesStatic().redfish_hidden()) {
continue;
}
if (GetTlbmcConfig()
.sensor_collector_module()
.sensor_collection_append_sub_module()
.per_sensor_append_mode() &&
!sensor_ptr->GetSensorAttributesStatic()
.append_to_sensor_collection()) {
continue;
}
valid_sensors_count++;
nlohmann::json::object_t member;
member["@odata.id"] = absl::StrCat(sensor_collection_url, "/", key);
members->push_back(std::move(member));
}
json["Members@odata.count"] = members_count + valid_sensors_count;
}
void HandleSensor(const RedfishApp& app, const RedfishRequest& req,
RedfishResponse& resp, const std::string& chassis_id,
const std::string& sensor_id) {
const Store& store = *app.GetStore();
std::shared_ptr<const Sensor> sensor =
store.GetSensorByConfigKeyAndSensorKey(chassis_id, sensor_id);
if (sensor == nullptr ||
sensor->GetSensorAttributesStatic().redfish_hidden()) {
resp.SetToNotFound(
absl::StrFormat("Chassis %s's sensor %s not found in tlBMC Store",
chassis_id, sensor_id));
return;
}
nlohmann::json::json_pointer sensor_pointer("");
FillResponseWithSensorData(sensor, sensor_pointer, resp, store);
}
void SetSuccessResponse(RedfishResponse& resp, absl::string_view message,
absl::string_view chassis_id,
absl::string_view sensor_id) {
nlohmann::json success = {{{"@odata.type", "#Message.v1_1_1.Message"},
{"Message", message},
{"MessageArgs", nlohmann::json::array_t()},
{"MessageId", "Base.1.13.0.Success"},
{"MessageSeverity", "OK"},
{"Resolution", "None"}}};
resp.SetKeyInJsonBody("/@Message.ExtendedInfo", success);
resp.SetKeyInJsonBody("/@odata.id",
CreateUrl({"redfish", "v1", "Chassis", chassis_id,
"Sensors", sensor_id}));
}
const nlohmann::json* FindSensorOverrideControl(const nlohmann::json& body) {
auto oem_it = body.find("Oem");
if (oem_it == body.end() || !oem_it->is_object()) {
return nullptr;
}
auto google_it = oem_it->find("Google");
if (google_it == oem_it->end() || !google_it->is_object()) {
return nullptr;
}
auto override_it = google_it->find("SensorValueOverridden");
if (override_it == google_it->end()) {
return nullptr;
}
return &(*override_it);
}
absl::StatusOr<SensorValue> ParseReadingValue(
const nlohmann::json& reading_json, const Sensor& sensor) {
SensorValue val;
if (sensor.GetSensorAttributesStatic().unit() == UNIT_COUNT) {
if (!reading_json.is_number_integer()) {
return absl::InvalidArgumentError(
"Reading must be an integer for count unit");
}
val.set_count(reading_json.get<int64_t>());
} else {
if (!reading_json.is_number()) {
return absl::InvalidArgumentError("Reading must be a number");
}
val.set_reading(reading_json.get<double>());
}
return val;
}
// Handles PATCH requests configuring or clearing software sensor overrides.
// Explicit behavior:
// 1. If SensorValueOverridden is true, a 'Reading' field MUST be provided.
// 2. If SensorValueOverridden is false, a 'Reading' field MUST NOT be provided.
void HandleSensorOverridePatch(Store& store, const Sensor& sensor,
const nlohmann::json& body,
const nlohmann::json& override_val_json,
RedfishResponse& resp,
absl::string_view chassis_id,
absl::string_view sensor_id) {
if (!absl::GetFlag(FLAGS_enable_tlbmc_sensor_override)) {
resp.SetToForbidden(
"Sensor error injection and software overrides are disabled on "
"production images. Use a dev-signed image or enable "
"--enable_tlbmc_sensor_override.");
return;
}
if (!override_val_json.is_boolean()) {
resp.SetToBadRequest("SensorValueOverridden must be a boolean");
return;
}
bool is_override = override_val_json.get<bool>();
auto reading_it = body.find("Reading");
// Behavior 1: If Override is false, we MUST NOT receive a reading.
if (!is_override) {
if (reading_it != body.end()) {
resp.SetToBadRequest(
"Clearing override must not include 'Reading' field");
return;
}
absl::Status status = store.ClearSensorOverride(std::string(sensor_id));
if (!status.ok()) {
resp.SetToAbslStatus(status);
return;
}
SetSuccessResponse(resp, "Sensor override successfully cleared.",
chassis_id, sensor_id);
return;
}
// Behavior 2: If Override is true, we MUST provide a reading.
if (reading_it == body.end()) {
resp.SetToBadRequest("Setting override requires 'Reading' field");
return;
}
absl::StatusOr<SensorValue> val = ParseReadingValue(*reading_it, sensor);
if (!val.ok()) {
resp.SetToBadRequest(val.status().message());
return;
}
absl::Status status = store.SetSensorOverride(std::string(sensor_id), *val);
if (!status.ok()) {
resp.SetToAbslStatus(status);
return;
}
SetSuccessResponse(resp, "Sensor override successfully created.", chassis_id,
sensor_id);
}
void HandleSensorPatch(const RedfishApp& app, const RedfishRequest& req,
RedfishResponse& resp, const std::string& chassis_id,
const std::string& sensor_id) {
Store& store = *app.GetStore();
std::shared_ptr<const Sensor> sensor =
store.GetSensorByConfigKeyAndSensorKey(chassis_id, sensor_id);
if (sensor == nullptr ||
sensor->GetSensorAttributesStatic().redfish_hidden()) {
resp.SetToNotFound(
absl::StrFormat("Chassis %s's sensor %s not found in tlBMC Store",
chassis_id, sensor_id));
return;
}
nlohmann::json body = nlohmann::json::parse(req.Body(), nullptr, false);
if (body.is_discarded()) {
resp.SetToBadRequest("Request body is not valid JSON");
return;
}
// Check for OEM Google Override control.
const nlohmann::json* override_json = FindSensorOverrideControl(body);
if (override_json != nullptr) {
HandleSensorOverridePatch(store, *sensor, body, *override_json, resp,
chassis_id, sensor_id);
return;
}
// Standard sensor write PATCH.
auto reading_it = body.find("Reading");
if (reading_it == body.end()) {
resp.SetToBadRequest("Request does not contain 'Reading' field");
return;
}
absl::StatusOr<SensorValue> val = ParseReadingValue(*reading_it, *sensor);
if (!val.ok()) {
resp.SetToBadRequest(val.status().message());
return;
}
absl::Status status = store.WriteToSensor(sensor_id, *val);
if (!status.ok()) {
resp.SetToAbslStatus(status);
return;
}
SetSuccessResponse(resp, "The request completed successfully.", chassis_id,
sensor_id);
}
std::string_view GetOemSensorStateString(Status status) {
switch (status) {
case STATUS_STALE:
return "Stale";
case STATUS_CREATION_FAILED:
return "CreationFailed";
case STATUS_DRIVER_READ_ERROR:
return "DriverReadError";
case STATUS_INVALID_DATA:
return "InvalidData";
case STATUS_OTHER_ERROR:
return "OtherError";
default:
return "Unknown";
}
}
} // namespace
void FillResponseWithSensorData(
const std::shared_ptr<const Sensor>& sensor,
const nlohmann::json::json_pointer& sensor_pointer, RedfishResponse& resp,
const Store& store) {
const SensorAttributesDynamic attributes_dynamic =
sensor->GetSensorAttributesDynamic();
const Status status = attributes_dynamic.state().status();
switch (status) {
case STATUS_READY: {
std::string_view health = kHealthOk;
double reading = sensor->GetSensorData()->reading();
for (const auto& config :
attributes_dynamic.thresholds().threshold_configs()) {
if (!config.has_value()) {
continue;
}
const auto type = config.type();
const double value = config.value();
if ((type == THRESHOLD_TYPE_UPPER_CRITICAL && reading > value) ||
(type == THRESHOLD_TYPE_LOWER_CRITICAL && reading < value)) {
health = kHealthCritical;
break;
}
if ((type == THRESHOLD_TYPE_UPPER_NON_CRITICAL && reading > value) ||
(type == THRESHOLD_TYPE_LOWER_NON_CRITICAL && reading < value)) {
health = kHealthWarning;
}
}
resp.SetKeyInJsonBody(sensor_pointer / "Status" / "Health", health);
resp.SetKeyInJsonBody(sensor_pointer / "Status" / "State", "Enabled");
resp.SetKeyInJsonBody(sensor_pointer / "Reading", reading);
break;
}
case STATUS_CREATION_PENDING:
resp.SetKeyInJsonBody(sensor_pointer / "Status" / "State", "Absent");
resp.SetKeyInJsonBody(sensor_pointer / "Reading", nullptr);
break;
default: {
// We will always return a sensor regardless of status. For unhealthy
// sensors, we will return a Disabled status and a detailed error message.
nlohmann::json error_message_json;
error_message_json["@odata.type"] = "#Message.v1_1_1.Message";
error_message_json["MessageId"] = "Base.1.13.0.InternalError";
error_message_json["Message"] = absl::StrFormat(
"Sensor %s is not ready in tlBMC Store: %s", sensor->GetKey(),
attributes_dynamic.state().status_message());
resp.SetErrorMessage(error_message_json);
resp.SetKeyInJsonBody(sensor_pointer / "Status" / "State", "Disabled");
resp.SetKeyInJsonBody(
sensor_pointer / "Status" / "Oem" / "Google" / "SensorState",
GetOemSensorStateString(status));
resp.SetKeyInJsonBody(sensor_pointer / "Reading", nullptr);
break;
}
}
const SensorAttributesStatic& attributes_static =
sensor->GetSensorAttributesStatic();
std::string chassis_id =
attributes_static.entity_common_config().board_config_key();
std::string sensor_url =
absl::Substitute("/redfish/v1/Chassis/$0/Sensors/$1", chassis_id,
attributes_static.attributes().key());
resp.SetKeyInJsonBody(sensor_pointer / "@odata.id", sensor_url);
resp.SetKeyInJsonBody(sensor_pointer / "@odata.type",
"#Sensor.v1_2_0.Sensor");
std::string sensor_name =
GetTrimmedSensorName(attributes_static.attributes().key());
resp.SetKeyInJsonBody(sensor_pointer / "Name", sensor_name);
resp.SetKeyInJsonBody(sensor_pointer / "Id",
attributes_static.attributes().key());
resp.SetKeyInJsonBody(sensor_pointer / "Description", "Sensor");
if (!attributes_static.related_item().id().empty()) {
nlohmann::json::array_t related_items;
nlohmann::json::object_t related_item;
switch (attributes_static.related_item().type()) {
case RESOURCE_TYPE_FAN:
related_item["@odata.id"] = CreateUrl(
{"redfish", "v1", "Chassis", chassis_id, "ThermalSubsystem", "Fans",
attributes_static.related_item().id()});
break;
case RESOURCE_TYPE_BOARD:
related_item["@odata.id"] =
CreateUrl({"redfish", "v1", "Chassis",
attributes_static.related_item().id()});
break;
case RESOURCE_TYPE_PROCESSOR: {
std::string processor_redfish_id(attributes_static.related_item().id());
// Resolve redfish_id from the parent board's topology if available.
absl::StatusOr<const TopologyConfigNode*> parent_board =
store.GetFruTopology(chassis_id);
if (parent_board.ok()) {
// Iterator type is google::protobuf::Map<std::string,
// std::string>::const_iterator.
auto redfish_it = (*parent_board)
->redfish_processor_ids()
.find(attributes_static.related_item().id());
if (redfish_it != (*parent_board)->redfish_processor_ids().end()) {
processor_redfish_id = redfish_it->second;
}
}
related_item["@odata.id"] =
CreateUrl({"redfish", "v1", "Systems",
attributes_static.related_item().system_id(),
"Processors", processor_redfish_id});
break;
}
case RESOURCE_TYPE_DIMM: {
related_item["@odata.id"] =
CreateUrl({"redfish", "v1", "Systems",
attributes_static.related_item().system_id(), "Memory",
attributes_static.related_item().id()});
break;
}
default:
break;
}
related_items.emplace_back(std::move(related_item));
resp.SetKeyInJsonBody(sensor_pointer / "RelatedItem", related_items);
}
std::string reading_units;
std::string reading_type;
// Reference: https://redfish.dmtf.org/schemas/v1/Sensor.v1_10_0.json
switch (attributes_static.unit()) {
case UNIT_UNKNOWN:
break;
case UNIT_DEGREE_CELSIUS:
reading_type = "Temperature";
reading_units = "Cel";
break;
case UNIT_VOLT:
reading_type = "Voltage";
reading_units = "V";
break;
case UNIT_WATT:
reading_type = "Power";
reading_units = "W";
break;
case UNIT_AMPERE:
reading_type = "Current";
reading_units = "A";
break;
case UNIT_REVOLUTION_PER_MINUTE:
reading_type = "Rotational";
reading_units = "RPM";
break;
case UNIT_PERCENT:
reading_type = "Percent";
reading_units = "%";
break;
case UNIT_COUNT:
reading_type = "Count";
reading_units = "Count";
break;
default:
break;
}
if (!reading_type.empty()) {
resp.SetKeyInJsonBody(sensor_pointer / "ReadingType", reading_type);
}
if (!reading_units.empty()) {
resp.SetKeyInJsonBody(sensor_pointer / "ReadingUnits", reading_units);
}
resp.SetBodyToJson();
if (attributes_static.has_reading_ranges()) {
for (const auto& reading_range :
attributes_static.reading_ranges().reading_range_configs()) {
if (reading_range.type() == READING_RANGE_TYPE_MAX) {
resp.SetKeyInJsonBody(sensor_pointer / "ReadingRangeMax",
reading_range.reading());
} else if (reading_range.type() == READING_RANGE_TYPE_MIN) {
resp.SetKeyInJsonBody(sensor_pointer / "ReadingRangeMin",
reading_range.reading());
}
}
}
if (attributes_dynamic.has_thresholds()) {
for (const auto& threshold :
attributes_dynamic.thresholds().threshold_configs()) {
switch (threshold.type()) {
case THRESHOLD_TYPE_LOWER_CRITICAL:
if (threshold.has_value()) {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "LowerCritical" / "Reading",
threshold.value());
} else {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "LowerCritical" / "Reading",
nullptr);
}
break;
case THRESHOLD_TYPE_LOWER_NON_CRITICAL:
if (threshold.has_value()) {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "LowerCaution" / "Reading",
threshold.value());
} else {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "LowerCaution" / "Reading",
nullptr);
}
break;
case THRESHOLD_TYPE_UPPER_NON_CRITICAL:
if (threshold.has_value()) {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "UpperCaution" / "Reading",
threshold.value());
} else {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "UpperCaution" / "Reading",
nullptr);
}
break;
case THRESHOLD_TYPE_UPPER_CRITICAL:
if (threshold.has_value()) {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "UpperCritical" / "Reading",
threshold.value());
} else {
resp.SetKeyInJsonBody(
sensor_pointer / "Thresholds" / "UpperCritical" / "Reading",
nullptr);
}
break;
default:
break;
}
}
}
if (sensor->IsOverridden()) {
resp.SetKeyInJsonBody(
sensor_pointer / "Oem" / "Google" / "SensorValueOverridden", true);
}
}
void RegisterRoutes(RedfishApp& app) {
TLBMC_ROUTE(app, "/redfish/v1/Chassis/<str>/Sensors/")
.methods(boost::beast::http::verb::get)(
absl::bind_front(HandleSensorCollection, std::cref(app)));
TLBMC_ROUTE(app, "/redfish/v1/Chassis/<str>/Sensors/<str>/")
.methods(boost::beast::http::verb::get)(
absl::bind_front(HandleSensor, std::cref(app)));
TLBMC_ROUTE(app, "/redfish/v1/Chassis/<str>/Sensors/<str>/")
.methods(boost::beast::http::verb::patch)(
absl::bind_front(HandleSensorPatch, std::cref(app)));
}
void RegisterRoutesAppendMode(RedfishApp& app) {
// Appends to the sensor collection returned by gBMCWeb
TLBMC_APPEND_GBMCWEB(
app.GetSmartRouter(), "/redfish/v1/Chassis/<str>/Sensors/",
boost::beast::http::verb::get,
absl::bind_front(HandleSensorCollectionAfterGbmcweb, std::cref(app)));
// Still register collection route for RedfishRoute-owning chassis
TLBMC_ROUTE(app, "/redfish/v1/Chassis/<str>/Sensors/")
.methods(boost::beast::http::verb::get)(
absl::bind_front(HandleSensorCollection, std::cref(app)));
// Owns the sensor resource that tlBMC models.
TLBMC_ROUTE(app, "/redfish/v1/Chassis/<str>/Sensors/<str>/")
.methods(boost::beast::http::verb::get)(
absl::bind_front(HandleSensor, std::cref(app)));
TLBMC_ROUTE(app, "/redfish/v1/Chassis/<str>/Sensors/<str>/")
.methods(boost::beast::http::verb::patch)(
absl::bind_front(HandleSensorPatch, std::cref(app)));
}
} // namespace milotic_tlbmc::sensor