virtual-sensor: Add ability to read a config from dbus
If we match on the right interface we attempt to add a virtual sensor
from a configuration on dbus.
As we do not want to take arbitrary expressions from dbus, we only match
on a pre-determined set of calculations that can be an exprtk expression
or a function. One is added in a later commit.
Signed-off-by: Rashmica Gupta <rashmica.g@gmail.com>
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I228f60fa0f484cd1e7be1aca3d097494c5168936
diff --git a/virtualSensor.cpp b/virtualSensor.cpp
index 53325ff..12adad2 100644
--- a/virtualSensor.cpp
+++ b/virtualSensor.cpp
@@ -13,6 +13,10 @@
static constexpr bool DEBUG = false;
static constexpr auto busName = "xyz.openbmc_project.VirtualSensor";
static constexpr auto sensorDbusPath = "/xyz/openbmc_project/sensors/";
+static constexpr auto entityManagerBusName =
+ "xyz.openbmc_project.EntityManager";
+static constexpr auto vsThresholdsIfaceSuffix = ".Thresholds";
+static constexpr std::array<const char*, 0> calculationIfaces = {};
using namespace phosphor::logging;
@@ -90,6 +94,135 @@
return assocs;
}
+template <typename U>
+struct VariantToNumber
+{
+ template <typename T>
+ U operator()(const T& t) const
+ {
+ if constexpr (std::is_convertible<T, U>::value)
+ {
+ return static_cast<U>(t);
+ }
+ throw std::invalid_argument("Invalid number type in config\n");
+ }
+};
+
+template <typename U>
+U getNumberFromConfig(const PropertyMap& map, const std::string& name,
+ bool required)
+{
+ if (auto itr = map.find(name); itr != map.end())
+ {
+ return std::visit(VariantToNumber<U>(), itr->second);
+ }
+ else if (required)
+ {
+ log<level::ERR>("Required field missing in config",
+ entry("NAME=%s", name.c_str()));
+ throw std::invalid_argument("Required field missing in config");
+ }
+ return std::numeric_limits<U>::quiet_NaN();
+}
+
+bool isCalculationType(const std::string& interface)
+{
+ auto itr = std::find(calculationIfaces.begin(), calculationIfaces.end(),
+ interface);
+ if (itr != calculationIfaces.end())
+ {
+ return true;
+ }
+ return false;
+}
+
+const std::string getThresholdType(const std::string& direction,
+ uint64_t severity)
+{
+ std::string threshold;
+ std::string suffix;
+ static const std::array thresholdTypes{"Warning", "Critical",
+ "PerformanceLoss", "SoftShutdown",
+ "HardShutdown"};
+
+ if (severity >= thresholdTypes.size())
+ {
+ throw std::invalid_argument(
+ "Invalid threshold severity specified in entity manager");
+ }
+ threshold = thresholdTypes[severity];
+
+ if (direction == "less than")
+ {
+ suffix = "Low";
+ }
+ else if (direction == "greater than")
+ {
+ suffix = "High";
+ }
+ else
+ {
+ throw std::invalid_argument(
+ "Invalid threshold direction specified in entity manager");
+ }
+ return threshold + suffix;
+}
+
+void parseThresholds(Json& thresholds, const PropertyMap& propertyMap)
+{
+ std::string direction;
+
+ auto severity =
+ getNumberFromConfig<uint64_t>(propertyMap, "Severity", true);
+ auto value = getNumberFromConfig<double>(propertyMap, "Value", true);
+
+ auto itr = propertyMap.find("Direction");
+ if (itr != propertyMap.end())
+ {
+ direction = std::get<std::string>(itr->second);
+ }
+
+ auto threshold = getThresholdType(direction, severity);
+ thresholds[threshold] = value;
+}
+
+void VirtualSensor::parseConfigInterface(const PropertyMap& propertyMap,
+ const std::string& sensorType,
+ const std::string& interface)
+{
+ /* Parse sensors / DBus params */
+ if (auto itr = propertyMap.find("Sensors"); itr != propertyMap.end())
+ {
+ auto sensors = std::get<std::vector<std::string>>(itr->second);
+ for (auto sensor : sensors)
+ {
+ std::replace(sensor.begin(), sensor.end(), ' ', '_');
+ auto sensorObjPath = sensorDbusPath + sensorType + "/" + sensor;
+
+ auto paramPtr =
+ std::make_unique<SensorParam>(bus, sensorObjPath, this);
+ symbols.create_variable(sensor);
+ paramMap.emplace(std::move(sensor), std::move(paramPtr));
+ }
+ }
+ /* Get expression string */
+ if (!isCalculationType(interface))
+ {
+ throw std::invalid_argument("Invalid expression in interface");
+ }
+ exprStr = interface;
+
+ /* Get optional min and max input and output values */
+ ValueIface::maxValue(
+ getNumberFromConfig<double>(propertyMap, "MaxValue", false));
+ ValueIface::minValue(
+ getNumberFromConfig<double>(propertyMap, "MinValue", false));
+ maxValidInput =
+ getNumberFromConfig<double>(propertyMap, "MaxValidInput", false);
+ minValidInput =
+ getNumberFromConfig<double>(propertyMap, "MinValidInput", false);
+}
+
void VirtualSensor::initVirtualSensor(const Json& sensorConfig,
const std::string& objPath)
{
@@ -211,12 +344,53 @@
printParams(paramMap);
}
+void VirtualSensor::initVirtualSensor(const InterfaceMap& interfaceMap,
+ const std::string& objPath,
+ const std::string& sensorType,
+ const std::string& calculationIface)
+{
+ Json thresholds;
+ const std::string vsThresholdsIntf =
+ calculationIface + vsThresholdsIfaceSuffix;
+
+ for (const auto& [interface, propertyMap] : interfaceMap)
+ {
+ /* Each threshold is on it's own interface with a number as a suffix
+ * eg xyz.openbmc_project.Configuration.ModifiedMedian.Thresholds1 */
+ if (interface.find(vsThresholdsIntf) != std::string::npos)
+ {
+ parseThresholds(thresholds, propertyMap);
+ }
+ else if (interface == calculationIface)
+ {
+ parseConfigInterface(propertyMap, sensorType, interface);
+ }
+ }
+
+ createThresholds(thresholds, objPath);
+ symbols.add_constants();
+ symbols.add_package(vecopsPackage);
+ expression.register_symbol_table(symbols);
+
+ /* Print all parameters for debug purpose only */
+ if (DEBUG)
+ {
+ printParams(paramMap);
+ }
+}
+
void VirtualSensor::setSensorValue(double value)
{
value = std::clamp(value, ValueIface::minValue(), ValueIface::maxValue());
ValueIface::value(value);
}
+double VirtualSensor::calculateValue()
+{
+ // Placeholder until calculation types are added
+ return std::numeric_limits<double>::quiet_NaN();
+}
+
void VirtualSensor::updateVirtualSensor()
{
for (auto& param : paramMap)
@@ -233,13 +407,18 @@
throw std::invalid_argument("ParamName not found in symbols");
}
}
- double val = expression.value();
+ auto itr =
+ std::find(calculationIfaces.begin(), calculationIfaces.end(), exprStr);
+ auto val = (itr == calculationIfaces.end()) ? expression.value()
+ : calculateValue();
/* Set sensor value to dbus interface */
setSensorValue(val);
if (DEBUG)
+ {
std::cout << "Sensor value is " << val << "\n";
+ }
/* Check sensor thresholds and log required message */
checkThresholds(val, perfLossIface);
@@ -317,6 +496,49 @@
}
}
+ManagedObjectType VirtualSensors::getObjectsFromDBus()
+{
+ ManagedObjectType objects;
+
+ try
+ {
+ auto method = bus.new_method_call(entityManagerBusName, "/",
+ "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects");
+ auto reply = bus.call(method);
+ reply.read(objects);
+ }
+ catch (const sdbusplus::exception::SdBusError& ex)
+ {
+ // If entity manager isn't running yet, keep going.
+ if (std::string("org.freedesktop.DBus.Error.ServiceUnknown") !=
+ ex.name())
+ {
+ throw ex.name();
+ }
+ }
+
+ return objects;
+}
+
+void VirtualSensors::propertiesChanged(sdbusplus::message::message& msg)
+{
+ std::string path;
+ PropertyMap properties;
+
+ msg.read(path, properties);
+
+ /* We get multiple callbacks for one sensor. 'Type' is a required field and
+ * is a unique label so use to to only proceed once per sensor */
+ if (properties.contains("Type"))
+ {
+ if (isCalculationType(path))
+ {
+ createVirtualSensorsFromDBus(path);
+ }
+ }
+}
+
/** @brief Parsing Virtual Sensor config JSON file */
Json VirtualSensors::parseConfigFile(const std::string configFile)
{
@@ -325,7 +547,7 @@
{
log<level::ERR>("config JSON file not found",
entry("FILENAME=%s", configFile.c_str()));
- throw std::exception{};
+ return {};
}
auto data = Json::parse(jsonFile, nullptr, false);
@@ -351,14 +573,168 @@
{"airflow", ValueIface::Unit::CFM},
{"pressure", ValueIface::Unit::Pascals}};
+const std::string getSensorTypeFromUnit(const std::string& unit)
+{
+ std::string unitPrefix = "xyz.openbmc_project.Sensor.Value.Unit.";
+ for (auto [type, unitObj] : unitMap)
+ {
+ auto unitPath = ValueIface::convertUnitToString(unitObj);
+ if (unitPath == (unitPrefix + unit))
+ {
+ return type;
+ }
+ }
+ return "";
+}
+
+void VirtualSensors::setupMatches()
+{
+ /* Already setup */
+ if (!this->matches.empty())
+ {
+ return;
+ }
+
+ /* Setup matches */
+ auto eventHandler = [this](sdbusplus::message::message& message) {
+ if (message.is_method_error())
+ {
+ log<level::ERR>("Callback method error");
+ return;
+ }
+ this->propertiesChanged(message);
+ };
+
+ for (const char* iface : calculationIfaces)
+ {
+ auto match = std::make_unique<sdbusplus::bus::match::match>(
+ bus,
+ sdbusplus::bus::match::rules::propertiesChangedNamespace(
+ "/xyz/openbmc_project/inventory", iface),
+ eventHandler);
+ this->matches.emplace_back(std::move(match));
+ }
+}
+
+void VirtualSensors::createVirtualSensorsFromDBus(
+ const std::string& calculationIface)
+{
+ if (calculationIface.empty())
+ {
+ log<level::ERR>("No calculation type supplied");
+ return;
+ }
+ auto objects = getObjectsFromDBus();
+
+ /* Get virtual sensors config data */
+ for (const auto& [path, interfaceMap] : objects)
+ {
+ auto objpath = static_cast<std::string>(path);
+ std::string name = path.filename();
+ std::string sensorType, sensorUnit;
+
+ /* Find Virtual Sensor interfaces */
+ if (!interfaceMap.contains(calculationIface))
+ {
+ continue;
+ }
+ if (name.empty())
+ {
+ log<level::ERR>(
+ "Virtual Sensor name not found in entity manager config");
+ continue;
+ }
+ if (virtualSensorsMap.contains(name))
+ {
+ log<level::ERR>("A virtual sensor with this name already exists",
+ entry("NAME=%s", name.c_str()));
+ continue;
+ }
+
+ /* Extract the virtual sensor type as we need this to initialize the
+ * sensor */
+ for (const auto& [interface, propertyMap] : interfaceMap)
+ {
+ if (interface != calculationIface)
+ {
+ continue;
+ }
+ auto itr = propertyMap.find("Units");
+ if (itr != propertyMap.end())
+ {
+ sensorUnit = std::get<std::string>(itr->second);
+ break;
+ }
+ }
+ sensorType = getSensorTypeFromUnit(sensorUnit);
+ if (sensorType.empty())
+ {
+ log<level::ERR>("Sensor unit is not supported",
+ entry("TYPE=%s", sensorUnit.c_str()));
+ continue;
+ }
+
+ try
+ {
+ auto virtObjPath = sensorDbusPath + sensorType + "/" + name;
+
+ auto virtualSensorPtr = std::make_unique<VirtualSensor>(
+ bus, virtObjPath.c_str(), interfaceMap, name, sensorType,
+ calculationIface);
+ log<level::INFO>("Added a new virtual sensor",
+ entry("NAME=%s", name.c_str()));
+ virtualSensorPtr->updateVirtualSensor();
+
+ /* Initialize unit value for virtual sensor */
+ virtualSensorPtr->ValueIface::unit(unitMap[sensorType]);
+ virtualSensorPtr->emit_object_added();
+
+ virtualSensorsMap.emplace(name, std::move(virtualSensorPtr));
+
+ /* Setup match for interfaces removed */
+ auto intfRemoved = [this, objpath,
+ name](sdbusplus::message::message& message) {
+ if (!virtualSensorsMap.contains(name))
+ {
+ return;
+ }
+ sdbusplus::message::object_path path;
+ message.read(path);
+ if (static_cast<const std::string&>(path) == objpath)
+ {
+ log<level::INFO>("Removed a virtual sensor",
+ entry("NAME=%s", name.c_str()));
+ virtualSensorsMap.erase(name);
+ }
+ };
+ auto matchOnRemove = std::make_unique<sdbusplus::bus::match::match>(
+ bus,
+ sdbusplus::bus::match::rules::interfacesRemoved() +
+ sdbusplus::bus::match::rules::argNpath(0, objpath),
+ intfRemoved);
+ /* TODO: slight race condition here. Check that the config still
+ * exists */
+ this->matches.emplace_back(std::move(matchOnRemove));
+ }
+ catch (std::invalid_argument& ia)
+ {
+ log<level::ERR>("Failed to set up virtual sensor",
+ entry("Error=%s", ia.what()));
+ }
+ }
+}
+
void VirtualSensors::createVirtualSensors()
{
static const Json empty{};
auto data = parseConfigFile(VIRTUAL_SENSOR_CONFIG_FILE);
+
// print values
if (DEBUG)
+ {
std::cout << "Config json data:\n" << data << "\n\n";
+ }
/* Get virtual sensors config data */
for (const auto& j : data)
@@ -366,6 +742,29 @@
auto desc = j.value("Desc", empty);
if (!desc.empty())
{
+ if (desc.value("Config", "") == "D-Bus")
+ {
+ /* Look on D-Bus for a virtual sensor config. Set up matches
+ * first because the configs may not be on D-Bus yet and we
+ * don't want to miss them */
+ setupMatches();
+
+ if (desc.contains("Type"))
+ {
+ auto path = "xyz.openbmc_project.Configuration." +
+ desc.value("Type", "");
+ if (!isCalculationType(path))
+ {
+ log<level::ERR>(
+ "Invalid calculation type supplied\n",
+ entry("TYPE=%s", desc.value("Type", "").c_str()));
+ continue;
+ }
+ createVirtualSensorsFromDBus(path);
+ }
+ continue;
+ }
+
std::string sensorType = desc.value("SensorType", "");
std::string name = desc.value("Name", "");
std::replace(name.begin(), name.end(), ' ', '_');
diff --git a/virtualSensor.hpp b/virtualSensor.hpp
index c28e547..ada20d9 100644
--- a/virtualSensor.hpp
+++ b/virtualSensor.hpp
@@ -17,6 +17,17 @@
namespace virtualSensor
{
+using BasicVariantType =
+ std::variant<std::string, int64_t, uint64_t, double, int32_t, uint32_t,
+ int16_t, uint16_t, uint8_t, bool, std::vector<std::string>>;
+
+using PropertyMap = std::map<std::string, BasicVariantType>;
+
+using InterfaceMap = std::map<std::string, PropertyMap>;
+
+using ManagedObjectType =
+ std::map<sdbusplus::message::object_path, InterfaceMap>;
+
using Json = nlohmann::json;
template <typename... T>
@@ -88,6 +99,25 @@
initVirtualSensor(sensorConfig, objPath);
}
+ /** @brief Constructs VirtualSensor
+ *
+ * @param[in] bus - Handle to system dbus
+ * @param[in] objPath - The Dbus path of sensor
+ * @param[in] ifacemap - All the sensor information
+ * @param[in] name - Virtual sensor name
+ * @param[in] type - Virtual sensor type/unit
+ * @param[in] calcType - Calculation used to calculate sensor value
+ *
+ */
+ VirtualSensor(sdbusplus::bus::bus& bus, const char* objPath,
+ const InterfaceMap& ifacemap, const std::string& name,
+ const std::string& type, const std::string& calculationType) :
+ ValueObject(bus, objPath, action::defer_emit),
+ bus(bus), name(name)
+ {
+ initVirtualSensor(ifacemap, objPath, type, calculationType);
+ }
+
/** @brief Set sensor value */
void setSensorValue(double value);
/** @brief Update sensor at regular intrval */
@@ -111,6 +141,10 @@
exprtk::expression<double> expression{};
/** @brief The vecops package so the expression can use vectors */
exprtk::rtl::vecops::package<double> vecopsPackage;
+ /** @brief The maximum valid value for an input sensor **/
+ double maxValidInput = std::numeric_limits<double>::infinity();
+ /** @brief The minimum valid value for an input sensor **/
+ double minValidInput = -std::numeric_limits<double>::infinity();
/** @brief The critical threshold interface object */
std::unique_ptr<Threshold<CriticalObject>> criticalIface;
@@ -132,8 +166,22 @@
void initVirtualSensor(const Json& sensorConfig,
const std::string& objPath);
+ /** @brief Read config from interface map and initialize sensor data
+ * for each virtual sensor
+ */
+ void initVirtualSensor(const InterfaceMap& interfaceMap,
+ const std::string& objPath,
+ const std::string& sensorType,
+ const std::string& calculationType);
+
+ /** @brief Returns which calculation function or expression to use */
+ double calculateValue();
/** @brief create threshold objects from json config */
void createThresholds(const Json& threshold, const std::string& objPath);
+ /** @brief parse config from entity manager **/
+ void parseConfigInterface(const PropertyMap& propertyMap,
+ const std::string& sensorType,
+ const std::string& interface);
/** @brief Check Sensor threshold and update alarm and log */
template <typename V, typename T>
@@ -201,19 +249,29 @@
{
createVirtualSensors();
}
+ /** @brief Calls createVirtualSensor when interface added */
+ void propertiesChanged(sdbusplus::message::message& msg);
private:
/** @brief sdbusplus bus client connection. */
sdbusplus::bus::bus& bus;
+ /** @brief Get virual sensor config from DBus**/
+ ManagedObjectType getObjectsFromDBus();
/** @brief Parsing virtual sensor config JSON file */
Json parseConfigFile(const std::string configFile);
+ /** @brief Matches for virtual sensors */
+ std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
/** @brief Map of the object VirtualSensor */
std::unordered_map<std::string, std::unique_ptr<VirtualSensor>>
virtualSensorsMap;
- /** @brief Create list of virtual sensors */
+ /** @brief Create list of virtual sensors from JSON config*/
void createVirtualSensors();
+ /** @brief Create list of virtual sensors from DBus config */
+ void createVirtualSensorsFromDBus(const std::string& calculationType);
+ /** @brief Setup matches for virtual sensors */
+ void setupMatches();
};
} // namespace virtualSensor