associations: Add conditions support
Allow there to be multiple associations files that are selected based on
an inventory property condition specified inside of them. The file(s)
needs to be located in the same directory as the default associations
file, but can have any name as long as it ends in .json. If a
conditional associations file is found, the default associations file is
ignored.
For example:
{
"condition":
{
"path": "system/chassis/motherboard",
"interface": "xyz.openbmc_project.Inventory.Decorator.Asset",
"property": "Model",
"values": [
"ModelA",
"ModelB"
]
},
"associations":
[
// The same associations syntax as described above.
]
}
This states that the associations in this file are valid if the
motherboard inventory item has a Model property with a value of either
ModelA or ModelB.
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Ib0f32815dee718ea268715896b5470ed2f25119e
diff --git a/README.md b/README.md
index a12f0c4..7cd4892 100644
--- a/README.md
+++ b/README.md
@@ -210,6 +210,63 @@
```
+In the case where different systems that require different associations reside
+in the same flash image, multiple JSON files must be used. These files must be
+in the same directory as the default associations file would go, but it does
+not matter what they are named as long as the name ends in '.json'. Each file
+then contains a 'condition' entry that specifies an inventory path, interface,
+property, and list of values. If the actual value of that property is in the
+list of values, then the condition is met and those associations are activated.
+
+If a file with a conditions section is found, then the default associations
+file is ignored. The end result is that associations are only ever loaded from
+one file, either the default file if there aren't any files with conditions in
+them, or the first file that had a condition that matched.
+
+An example is:
+
+```
+{
+ "condition":
+ {
+ "path": "system/chassis/motherboard",
+ "interface": "xyz.openbmc_project.Inventory.Decorator.Asset",
+ "property": "Model",
+ "values": [
+ "ModelA",
+ "ModelB"
+ ]
+ },
+ "associations":
+ [
+ {
+ "path": "system/chassis/motherboard/cpu0/core1",
+ "endpoints":
+ [
+ {
+ "types":
+ {
+ "fType": "sensors",
+ "rType": "inventory"
+ },
+ "paths":
+ [
+ "/xyz/openbmc_project/sensors/temperature/p0_core0_temp"
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+This states that these associations are valid if the system/chassis/motherboard
+inventory object has a Model property with a value of either ModelA or
+ModelB.
+
+The values field supports the same types as in the inventory, so either a 'bool'
+(true/false), 'int64\_t,' 'string', or 'std::vector<uint8_t>'([1, 2]).
+
----
## Building
After running pimgen.py, build PIM using the following steps:
diff --git a/association_manager.cpp b/association_manager.cpp
index 91956ba..974b02c 100644
--- a/association_manager.cpp
+++ b/association_manager.cpp
@@ -1,6 +1,5 @@
#include "association_manager.hpp"
-#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <filesystem>
@@ -16,11 +15,22 @@
{
using namespace phosphor::logging;
using sdbusplus::exception::SdBusError;
+namespace fs = std::filesystem;
Manager::Manager(sdbusplus::bus::bus& bus, const std::string& jsonPath) :
_bus(bus), _jsonFile(jsonPath)
{
- load();
+ // If there aren't any conditional associations files, look for
+ // that default nonconditional one.
+ if (!loadConditions())
+ {
+ if (fs::exists(_jsonFile))
+ {
+ std::ifstream file{_jsonFile};
+ auto json = nlohmann::json::parse(file, nullptr, true);
+ load(json);
+ }
+ }
}
/**
@@ -37,15 +47,175 @@
}
}
-void Manager::load()
+bool Manager::loadConditions()
{
- // Load the contents of _jsonFile into _associations and throw
- // an exception on any problem.
+ auto dir = _jsonFile.parent_path();
- std::ifstream file{_jsonFile};
+ for (const auto& dirent : fs::recursive_directory_iterator(dir))
+ {
+ const auto& path = dirent.path();
+ if (path.extension() == ".json")
+ {
+ std::ifstream file{path};
+ auto json = nlohmann::json::parse(file, nullptr, true);
- auto json = nlohmann::json::parse(file, nullptr, true);
+ if (json.is_object() && json.contains("condition"))
+ {
+ const auto& conditionJSON = json.at("condition");
+ if (!conditionJSON.contains("path") ||
+ !conditionJSON.contains("interface") ||
+ !conditionJSON.contains("property") ||
+ !conditionJSON.contains("values"))
+ {
+ std::string msg =
+ "Invalid JSON in associations condition entry in " +
+ path.string() + ". Skipping file.";
+ log<level::ERR>(msg.c_str());
+ continue;
+ }
+ Condition c;
+ c.file = path;
+ c.path = conditionJSON["path"].get<std::string>();
+ if (c.path.front() != '/')
+ {
+ c.path = '/' + c.path;
+ }
+ fprintf(stderr, "found conditions file %s\n", c.file.c_str());
+ c.interface = conditionJSON["interface"].get<std::string>();
+ c.property = conditionJSON["property"].get<std::string>();
+
+ // The values are in an array, and need to be
+ // converted to an InterfaceVariantType.
+ for (const auto& value : conditionJSON["values"])
+ {
+ if (value.is_array())
+ {
+ std::vector<uint8_t> variantValue;
+ for (const auto& v : value)
+ {
+ variantValue.push_back(v.get<uint8_t>());
+ }
+ c.values.push_back(variantValue);
+ continue;
+ }
+
+ // Try the remaining types
+ auto s = value.get_ptr<const std::string*>();
+ auto i = value.get_ptr<const int64_t*>();
+ auto b = value.get_ptr<const bool*>();
+ if (s)
+ {
+ c.values.push_back(*s);
+ }
+ else if (i)
+ {
+ c.values.push_back(*i);
+ }
+ else if (b)
+ {
+ c.values.push_back(*b);
+ }
+ else
+ {
+ std::stringstream ss;
+ ss << "Invalid condition property value in " << c.file
+ << ": " << value;
+ log<level::ERR>(ss.str().c_str());
+ throw std::runtime_error(ss.str());
+ }
+ }
+
+ _conditions.push_back(std::move(c));
+ }
+ }
+ }
+
+ return !_conditions.empty();
+}
+
+bool Manager::conditionMatch(const sdbusplus::message::object_path& objectPath,
+ const Object& object)
+{
+ fs::path foundPath;
+ for (const auto& condition : _conditions)
+ {
+ if (condition.path != objectPath)
+ {
+ continue;
+ }
+
+ auto interface = std::find_if(object.begin(), object.end(),
+ [&condition](const auto& i) {
+ return i.first == condition.interface;
+ });
+ if (interface == object.end())
+ {
+ continue;
+ }
+
+ auto property =
+ std::find_if(interface->second.begin(), interface->second.end(),
+ [&condition](const auto& p) {
+ return condition.property == p.first;
+ });
+ if (property == interface->second.end())
+ {
+ continue;
+ }
+
+ auto match = std::find(condition.values.begin(), condition.values.end(),
+ property->second);
+ if (match != condition.values.end())
+ {
+ foundPath = condition.file;
+ break;
+ }
+ }
+
+ if (!foundPath.empty())
+ {
+ std::ifstream file{foundPath};
+ auto json = nlohmann::json::parse(file, nullptr, true);
+ load(json["associations"]);
+ _conditions.clear();
+ return true;
+ }
+
+ return false;
+}
+
+bool Manager::conditionMatch()
+{
+ fs::path foundPath;
+
+ for (const auto& condition : _conditions)
+ {
+ // Compare the actualValue field against the values in the
+ // values vector to see if there is a condition match.
+ auto found = std::find(condition.values.begin(), condition.values.end(),
+ condition.actualValue);
+ if (found != condition.values.end())
+ {
+ foundPath = condition.file;
+ break;
+ }
+ }
+
+ if (!foundPath.empty())
+ {
+ std::ifstream file{foundPath};
+ auto json = nlohmann::json::parse(file, nullptr, true);
+ load(json["associations"]);
+ _conditions.clear();
+ return true;
+ }
+
+ return false;
+}
+
+void Manager::load(const nlohmann::json& json)
+{
const std::string root{INVENTORY_ROOT};
for (const auto& jsonAssoc : json)
diff --git a/association_manager.hpp b/association_manager.hpp
index 2be0058..eda7e05 100644
--- a/association_manager.hpp
+++ b/association_manager.hpp
@@ -2,8 +2,14 @@
#include "config.h"
+#include "types.hpp"
+
+#include <nlohmann/json.hpp>
#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <any>
+#include <filesystem>
+
namespace phosphor
{
namespace inventory
@@ -47,6 +53,16 @@
class Manager
{
public:
+ struct Condition
+ {
+ std::string path;
+ std::string interface;
+ std::string property;
+ std::vector<InterfaceVariantType> values;
+ std::filesystem::path file;
+ InterfaceVariantType actualValue;
+ };
+
Manager() = delete;
~Manager() = default;
Manager(const Manager&) = delete;
@@ -95,13 +111,69 @@
return _associations;
}
+ /**
+ * @brief Returns the list of conditions
+ *
+ * @return vector<Condition>& - The conditions
+ */
+ std::vector<Condition>& getConditions()
+ {
+ return _conditions;
+ }
+
+ /**
+ * @brief Says if there are conditions that need to be met
+ * before an associations file is valid.
+ *
+ * @return bool - If there are pending conditions
+ */
+ bool pendingCondition() const
+ {
+ return !_conditions.empty();
+ }
+
+ /**
+ * @brief Checks if a pending condition is satisfied based on the
+ * path, interface, and property value of the object passed
+ * in.
+ *
+ * If it is valid, it will load the associations pointed to
+ * by that condition and erase the _conditions vector as
+ * there are no longer any pending conditions.
+ *
+ * @param[in] objectPath - The D-Bus path of the object to check
+ * @param[in] in object - The interface and properties of the object
+ *
+ * @return bool - If the object matched a condition
+ */
+ bool conditionMatch(const sdbusplus::message::object_path& objectPath,
+ const Object& object);
+
+ /**
+ * @brief Checks if a pending condition is satisfied based on if the
+ * actualValue field in the condition matches one of the values
+ * in the values field.
+ *
+ * The actualValue field was previously set by code based on the
+ * real property value of the specified interface on the specified
+ * path.
+ *
+ * If it is valid, it will load the associations pointed to
+ * by that condition and erase the _conditions vector as
+ * there are no longer any pending conditions.
+ *
+ * @return bool - If a condition was met
+ */
+ bool conditionMatch();
+
private:
/**
- * @brief Loads the association YAML into the _associations data
- * structure. This file is optional, so if it doesn't exist
- * it will just not load anything.
+ * @brief Loads the association JSON into the _associations data
+ * structure.
+ *
+ * @param[in] json - The associations JSON
*/
- void load();
+ void load(const nlohmann::json& json);
/**
* @brief Creates an instance of an org.openbmc.Associations
@@ -120,6 +192,13 @@
const std::string& reverseType, bool deferSignal);
/**
+ * @brief Looks for all JSON files in the associations directory that
+ * contain a valid association condition, and loads the
+ * conditions into the _conditions vector.
+ */
+ bool loadConditions();
+
+ /**
* @brief The map of association data that is loaded from its
* JSON definition. Association D-Bus objects will be
* created from this data.
@@ -140,12 +219,17 @@
/**
* @brief The path to the associations JSON File.
*/
- const std::string _jsonFile;
+ const std::filesystem::path _jsonFile;
/**
* A list of the inventory association paths that have already been handled.
*/
std::vector<std::string> _handled;
+
+ /**
+ * @brief Conditions that specify when an associations file is valid.
+ */
+ std::vector<Condition> _conditions;
};
} // namespace associations
diff --git a/generated.mako.cpp b/generated.mako.cpp
index fee8745..4adb55e 100644
--- a/generated.mako.cpp
+++ b/generated.mako.cpp
@@ -1,6 +1,7 @@
## This file is a template. The comment below is emitted
## into the rendered file; feel free to edit this file.
// This file was auto generated. Do not edit.
+#include "config.h"
#include "manager.hpp"
#include "utils.hpp"
#include "functor.hpp"
@@ -35,6 +36,11 @@
DeserializeInterface<
ServerObject<
${i.namespace()}>, SerialOps>::op
+#ifdef CREATE_ASSOCIATIONS
+ , GetPropertyValue<
+ ServerObject<
+ ${i.namespace()}>>::op
+#endif
)
},
% endfor
diff --git a/interface_ops.hpp b/interface_ops.hpp
index b16fa57..bc3e926 100644
--- a/interface_ops.hpp
+++ b/interface_ops.hpp
@@ -75,6 +75,28 @@
};
template <typename T, typename Enable = void>
+struct GetPropertyValue
+{
+ static InterfaceVariantType op(const std::string propertyName,
+ std::any& holder)
+ {
+ return InterfaceVariantType{};
+ }
+};
+
+template <typename T>
+struct GetPropertyValue<T, std::enable_if_t<HasProperties<T>::value>>
+{
+ static InterfaceVariantType op(const std::string propertyName,
+ std::any& holder)
+ {
+ auto& iface = *std::any_cast<std::shared_ptr<T>&>(holder);
+ auto property = iface.getPropertyByName(propertyName);
+ return convertVariant<InterfaceVariantType>(property);
+ }
+};
+
+template <typename T, typename Enable = void>
struct AssignInterface
{
static void op(const Interface&, std::any&, bool)
@@ -150,6 +172,8 @@
template <typename Ops>
using DeserializeInterfaceType =
std::add_pointer_t<decltype(DeserializeInterface<DummyInterface, Ops>::op)>;
+using GetPropertyValueType =
+ std::add_pointer_t<decltype(GetPropertyValue<DummyInterface>::op)>;
} // namespace manager
} // namespace inventory
diff --git a/manager.cpp b/manager.cpp
index a64030a..0fae387 100644
--- a/manager.cpp
+++ b/manager.cpp
@@ -250,11 +250,23 @@
updateInterfaces(absPath, objit->second, refit, newObj,
restoreFromCache);
#ifdef CREATE_ASSOCIATIONS
- if (newObj)
+ if (!_associations.pendingCondition() && newObj)
{
_associations.createAssociations(absPath,
_status != ManagerStatus::RUNNING);
}
+ else if (!restoreFromCache &&
+ _associations.conditionMatch(objit->first, objit->second))
+ {
+ // The objit path/interface/property matched a pending condition.
+ // Now the associations are valid so attempt to create them against
+ // all existing objects. If this was the restoreFromCache path,
+ // objit doesn't contain property values so don't bother checking.
+ std::for_each(_refs.begin(), _refs.end(), [this](const auto& ref) {
+ _associations.createAssociations(
+ ref.first, _status != ManagerStatus::RUNNING);
+ });
+ }
#endif
++objit;
}
@@ -365,6 +377,55 @@
{
auto restoreFromCache = true;
updateObjects(objects, restoreFromCache);
+
+#ifdef CREATE_ASSOCIATIONS
+ // There may be conditional associations waiting to be loaded
+ // based on certain path/interface/property values. Now that
+ // _refs contains all objects with their property values, check
+ // which property values the conditions need and set them in the
+ // condition structure entries, using the actualValue field. Then
+ // the associations manager can check if the conditions are met.
+ if (_associations.pendingCondition())
+ {
+ ObjectReferences::iterator refIt;
+ InterfaceComposite::iterator ifaceIt;
+
+ auto& conditions = _associations.getConditions();
+ for (auto& condition : conditions)
+ {
+ refIt = _refs.find(_root + condition.path);
+ if (refIt != _refs.end())
+ {
+ ifaceIt = refIt->second.find(condition.interface);
+ }
+
+ if ((refIt != _refs.end()) && (ifaceIt != refIt->second.end()))
+ {
+ const auto& maker = _makers.find(condition.interface);
+ if (maker != _makers.end())
+ {
+ auto& getProperty =
+ std::get<GetPropertyValueType>(maker->second);
+
+ condition.actualValue =
+ getProperty(condition.property, ifaceIt->second);
+ }
+ }
+ }
+
+ // Check if a property value in a condition matches an
+ // actual property value just saved. If one did, now the
+ // associations file is valid so create its associations.
+ if (_associations.conditionMatch())
+ {
+ std::for_each(
+ _refs.begin(), _refs.end(), [this](const auto& ref) {
+ _associations.createAssociations(
+ ref.first, _status != ManagerStatus::RUNNING);
+ });
+ }
+ }
+#endif
}
}
diff --git a/manager.hpp b/manager.hpp
index ab1133c..744caa5 100644
--- a/manager.hpp
+++ b/manager.hpp
@@ -137,7 +137,12 @@
using Makers =
std::map<std::string, std::tuple<MakeInterfaceType, AssignInterfaceType,
SerializeInterfaceType<SerialOps>,
- DeserializeInterfaceType<SerialOps>>>;
+ DeserializeInterfaceType<SerialOps>
+#ifdef CREATE_ASSOCIATIONS
+ ,
+ GetPropertyValueType
+#endif
+ >>;
/** @brief Provides weak references to interface holders.
*