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/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)