1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 /// \file entity_manager.cpp
17
18 #include "entity_manager.hpp"
19
20 #include "overlay.hpp"
21 #include "topology.hpp"
22 #include "utils.hpp"
23 #include "variant_visitors.hpp"
24
25 #include <boost/algorithm/string/case_conv.hpp>
26 #include <boost/algorithm/string/classification.hpp>
27 #include <boost/algorithm/string/predicate.hpp>
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/algorithm/string/split.hpp>
30 #include <boost/asio/io_context.hpp>
31 #include <boost/asio/post.hpp>
32 #include <boost/asio/steady_timer.hpp>
33 #include <boost/container/flat_map.hpp>
34 #include <boost/container/flat_set.hpp>
35 #include <boost/range/iterator_range.hpp>
36 #include <nlohmann/json.hpp>
37 #include <sdbusplus/asio/connection.hpp>
38 #include <sdbusplus/asio/object_server.hpp>
39
40 #include <charconv>
41 #include <filesystem>
42 #include <fstream>
43 #include <functional>
44 #include <iostream>
45 #include <map>
46 #include <regex>
47 #include <variant>
48 constexpr const char* hostConfigurationDirectory = SYSCONF_DIR "configurations";
49 constexpr const char* configurationDirectory = PACKAGE_DIR "configurations";
50 constexpr const char* schemaDirectory = PACKAGE_DIR "configurations/schemas";
51 constexpr const char* tempConfigDir = "/tmp/configuration/";
52 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
53 constexpr const char* currentConfiguration = "/var/configuration/system.json";
54 constexpr const char* globalSchema = "global.json";
55
56 const boost::container::flat_map<const char*, probe_type_codes, CmpStr>
57 probeTypes{{{"FALSE", probe_type_codes::FALSE_T},
58 {"TRUE", probe_type_codes::TRUE_T},
59 {"AND", probe_type_codes::AND},
60 {"OR", probe_type_codes::OR},
61 {"FOUND", probe_type_codes::FOUND},
62 {"MATCH_ONE", probe_type_codes::MATCH_ONE}}};
63
64 static constexpr std::array<const char*, 6> settableInterfaces = {
65 "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
66 using JsonVariantType =
67 std::variant<std::vector<std::string>, std::vector<double>, std::string,
68 int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
69 uint16_t, uint8_t, bool>;
70
71 // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
72 // store reference to all interfaces so we can destroy them later
73 boost::container::flat_map<
74 std::string, std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>>
75 inventory;
76
77 // todo: pass this through nicer
78 std::shared_ptr<sdbusplus::asio::connection> systemBus;
79 nlohmann::json lastJson;
80 Topology topology;
81
82 boost::asio::io_context io;
83 // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
84
85 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
86 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
87
tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface> & iface)88 void tryIfaceInitialize(std::shared_ptr<sdbusplus::asio::dbus_interface>& iface)
89 {
90 try
91 {
92 iface->initialize();
93 }
94 catch (std::exception& e)
95 {
96 std::cerr << "Unable to initialize dbus interface : " << e.what()
97 << "\n"
98 << "object Path : " << iface->get_object_path() << "\n"
99 << "interface name : " << iface->get_interface_name() << "\n";
100 }
101 }
102
findProbeType(const std::string & probe)103 FoundProbeTypeT findProbeType(const std::string& probe)
104 {
105 boost::container::flat_map<const char*, probe_type_codes,
106 CmpStr>::const_iterator probeType;
107 for (probeType = probeTypes.begin(); probeType != probeTypes.end();
108 ++probeType)
109 {
110 if (probe.find(probeType->first) != std::string::npos)
111 {
112 return probeType;
113 }
114 }
115
116 return std::nullopt;
117 }
118
119 static std::shared_ptr<sdbusplus::asio::dbus_interface>
createInterface(sdbusplus::asio::object_server & objServer,const std::string & path,const std::string & interface,const std::string & parent,bool checkNull=false)120 createInterface(sdbusplus::asio::object_server& objServer,
121 const std::string& path, const std::string& interface,
122 const std::string& parent, bool checkNull = false)
123 {
124 // on first add we have no reason to check for null before add, as there
125 // won't be any. For dynamically added interfaces, we check for null so that
126 // a constant delete/add will not create a memory leak
127
128 auto ptr = objServer.add_interface(path, interface);
129 auto& dataVector = inventory[parent];
130 if (checkNull)
131 {
132 auto it = std::find_if(dataVector.begin(), dataVector.end(),
133 [](const auto& p) { return p.expired(); });
134 if (it != dataVector.end())
135 {
136 *it = ptr;
137 return ptr;
138 }
139 }
140 dataVector.emplace_back(ptr);
141 return ptr;
142 }
143
144 // writes output files to persist data
writeJsonFiles(const nlohmann::json & systemConfiguration)145 bool writeJsonFiles(const nlohmann::json& systemConfiguration)
146 {
147 std::filesystem::create_directory(configurationOutDir);
148 std::ofstream output(currentConfiguration);
149 if (!output.good())
150 {
151 return false;
152 }
153 output << systemConfiguration.dump(4);
154 output.close();
155 return true;
156 }
157
158 template <typename JsonType>
setJsonFromPointer(const std::string & ptrStr,const JsonType & value,nlohmann::json & systemConfiguration)159 bool setJsonFromPointer(const std::string& ptrStr, const JsonType& value,
160 nlohmann::json& systemConfiguration)
161 {
162 try
163 {
164 nlohmann::json::json_pointer ptr(ptrStr);
165 nlohmann::json& ref = systemConfiguration[ptr];
166 ref = value;
167 return true;
168 }
169 catch (const std::out_of_range&)
170 {
171 return false;
172 }
173 }
174
175 // template function to add array as dbus property
176 template <typename PropertyType>
addArrayToDbus(const std::string & name,const nlohmann::json & array,sdbusplus::asio::dbus_interface * iface,sdbusplus::asio::PropertyPermission permission,nlohmann::json & systemConfiguration,const std::string & jsonPointerString)177 void addArrayToDbus(const std::string& name, const nlohmann::json& array,
178 sdbusplus::asio::dbus_interface* iface,
179 sdbusplus::asio::PropertyPermission permission,
180 nlohmann::json& systemConfiguration,
181 const std::string& jsonPointerString)
182 {
183 std::vector<PropertyType> values;
184 for (const auto& property : array)
185 {
186 auto ptr = property.get_ptr<const PropertyType*>();
187 if (ptr != nullptr)
188 {
189 values.emplace_back(*ptr);
190 }
191 }
192
193 if (permission == sdbusplus::asio::PropertyPermission::readOnly)
194 {
195 iface->register_property(name, values);
196 }
197 else
198 {
199 iface->register_property(
200 name, values,
201 [&systemConfiguration,
202 jsonPointerString{std::string(jsonPointerString)}](
203 const std::vector<PropertyType>& newVal,
204 std::vector<PropertyType>& val) {
205 val = newVal;
206 if (!setJsonFromPointer(jsonPointerString, val,
207 systemConfiguration))
208 {
209 std::cerr << "error setting json field\n";
210 return -1;
211 }
212 if (!writeJsonFiles(systemConfiguration))
213 {
214 std::cerr << "error setting json file\n";
215 return -1;
216 }
217 return 1;
218 });
219 }
220 }
221
222 template <typename PropertyType>
addProperty(const std::string & name,const PropertyType & value,sdbusplus::asio::dbus_interface * iface,nlohmann::json & systemConfiguration,const std::string & jsonPointerString,sdbusplus::asio::PropertyPermission permission)223 void addProperty(const std::string& name, const PropertyType& value,
224 sdbusplus::asio::dbus_interface* iface,
225 nlohmann::json& systemConfiguration,
226 const std::string& jsonPointerString,
227 sdbusplus::asio::PropertyPermission permission)
228 {
229 if (permission == sdbusplus::asio::PropertyPermission::readOnly)
230 {
231 iface->register_property(name, value);
232 return;
233 }
234 iface->register_property(
235 name, value,
236 [&systemConfiguration,
237 jsonPointerString{std::string(jsonPointerString)}](
238 const PropertyType& newVal, PropertyType& val) {
239 val = newVal;
240 if (!setJsonFromPointer(jsonPointerString, val,
241 systemConfiguration))
242 {
243 std::cerr << "error setting json field\n";
244 return -1;
245 }
246 if (!writeJsonFiles(systemConfiguration))
247 {
248 std::cerr << "error setting json file\n";
249 return -1;
250 }
251 return 1;
252 });
253 }
254
createDeleteObjectMethod(const std::string & jsonPointerPath,const std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,sdbusplus::asio::object_server & objServer,nlohmann::json & systemConfiguration)255 void createDeleteObjectMethod(
256 const std::string& jsonPointerPath,
257 const std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
258 sdbusplus::asio::object_server& objServer,
259 nlohmann::json& systemConfiguration)
260 {
261 std::weak_ptr<sdbusplus::asio::dbus_interface> interface = iface;
262 iface->register_method(
263 "Delete", [&objServer, &systemConfiguration, interface,
264 jsonPointerPath{std::string(jsonPointerPath)}]() {
265 std::shared_ptr<sdbusplus::asio::dbus_interface> dbusInterface =
266 interface.lock();
267 if (!dbusInterface)
268 {
269 // this technically can't happen as the pointer is pointing to
270 // us
271 throw DBusInternalError();
272 }
273 nlohmann::json::json_pointer ptr(jsonPointerPath);
274 systemConfiguration[ptr] = nullptr;
275
276 // todo(james): dig through sdbusplus to find out why we can't
277 // delete it in a method call
278 boost::asio::post(io, [&objServer, dbusInterface]() mutable {
279 objServer.remove_interface(dbusInterface);
280 });
281
282 if (!writeJsonFiles(systemConfiguration))
283 {
284 std::cerr << "error setting json file\n";
285 throw DBusInternalError();
286 }
287 });
288 }
289
290 // adds simple json types to interface's properties
populateInterfaceFromJson(nlohmann::json & systemConfiguration,const std::string & jsonPointerPath,std::shared_ptr<sdbusplus::asio::dbus_interface> & iface,nlohmann::json & dict,sdbusplus::asio::object_server & objServer,sdbusplus::asio::PropertyPermission permission=sdbusplus::asio::PropertyPermission::readOnly)291 void populateInterfaceFromJson(
292 nlohmann::json& systemConfiguration, const std::string& jsonPointerPath,
293 std::shared_ptr<sdbusplus::asio::dbus_interface>& iface,
294 nlohmann::json& dict, sdbusplus::asio::object_server& objServer,
295 sdbusplus::asio::PropertyPermission permission =
296 sdbusplus::asio::PropertyPermission::readOnly)
297 {
298 for (const auto& [key, value] : dict.items())
299 {
300 auto type = value.type();
301 bool array = false;
302 if (value.type() == nlohmann::json::value_t::array)
303 {
304 array = true;
305 if (value.empty())
306 {
307 continue;
308 }
309 type = value[0].type();
310 bool isLegal = true;
311 for (const auto& arrayItem : value)
312 {
313 if (arrayItem.type() != type)
314 {
315 isLegal = false;
316 break;
317 }
318 }
319 if (!isLegal)
320 {
321 std::cerr << "dbus format error" << value << "\n";
322 continue;
323 }
324 }
325 if (type == nlohmann::json::value_t::object)
326 {
327 continue; // handled elsewhere
328 }
329
330 std::string path = jsonPointerPath;
331 path.append("/").append(key);
332 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
333 {
334 // all setable numbers are doubles as it is difficult to always
335 // create a configuration file with all whole numbers as decimals
336 // i.e. 1.0
337 if (array)
338 {
339 if (value[0].is_number())
340 {
341 type = nlohmann::json::value_t::number_float;
342 }
343 }
344 else if (value.is_number())
345 {
346 type = nlohmann::json::value_t::number_float;
347 }
348 }
349
350 switch (type)
351 {
352 case (nlohmann::json::value_t::boolean):
353 {
354 if (array)
355 {
356 // todo: array of bool isn't detected correctly by
357 // sdbusplus, change it to numbers
358 addArrayToDbus<uint64_t>(key, value, iface.get(),
359 permission, systemConfiguration,
360 path);
361 }
362
363 else
364 {
365 addProperty(key, value.get<bool>(), iface.get(),
366 systemConfiguration, path, permission);
367 }
368 break;
369 }
370 case (nlohmann::json::value_t::number_integer):
371 {
372 if (array)
373 {
374 addArrayToDbus<int64_t>(key, value, iface.get(), permission,
375 systemConfiguration, path);
376 }
377 else
378 {
379 addProperty(key, value.get<int64_t>(), iface.get(),
380 systemConfiguration, path,
381 sdbusplus::asio::PropertyPermission::readOnly);
382 }
383 break;
384 }
385 case (nlohmann::json::value_t::number_unsigned):
386 {
387 if (array)
388 {
389 addArrayToDbus<uint64_t>(key, value, iface.get(),
390 permission, systemConfiguration,
391 path);
392 }
393 else
394 {
395 addProperty(key, value.get<uint64_t>(), iface.get(),
396 systemConfiguration, path,
397 sdbusplus::asio::PropertyPermission::readOnly);
398 }
399 break;
400 }
401 case (nlohmann::json::value_t::number_float):
402 {
403 if (array)
404 {
405 addArrayToDbus<double>(key, value, iface.get(), permission,
406 systemConfiguration, path);
407 }
408
409 else
410 {
411 addProperty(key, value.get<double>(), iface.get(),
412 systemConfiguration, path, permission);
413 }
414 break;
415 }
416 case (nlohmann::json::value_t::string):
417 {
418 if (array)
419 {
420 addArrayToDbus<std::string>(key, value, iface.get(),
421 permission, systemConfiguration,
422 path);
423 }
424 else
425 {
426 addProperty(key, value.get<std::string>(), iface.get(),
427 systemConfiguration, path, permission);
428 }
429 break;
430 }
431 default:
432 {
433 std::cerr << "Unexpected json type in system configuration "
434 << key << ": " << value.type_name() << "\n";
435 break;
436 }
437 }
438 }
439 if (permission == sdbusplus::asio::PropertyPermission::readWrite)
440 {
441 createDeleteObjectMethod(jsonPointerPath, iface, objServer,
442 systemConfiguration);
443 }
444 tryIfaceInitialize(iface);
445 }
446
getPermission(const std::string & interface)447 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
448 {
449 return std::find(settableInterfaces.begin(), settableInterfaces.end(),
450 interface) != settableInterfaces.end()
451 ? sdbusplus::asio::PropertyPermission::readWrite
452 : sdbusplus::asio::PropertyPermission::readOnly;
453 }
454
createAddObjectMethod(const std::string & jsonPointerPath,const std::string & path,nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer,const std::string & board)455 void createAddObjectMethod(
456 const std::string& jsonPointerPath, const std::string& path,
457 nlohmann::json& systemConfiguration,
458 sdbusplus::asio::object_server& objServer, const std::string& board)
459 {
460 std::shared_ptr<sdbusplus::asio::dbus_interface> iface = createInterface(
461 objServer, path, "xyz.openbmc_project.AddObject", board);
462
463 iface->register_method(
464 "AddObject",
465 [&systemConfiguration, &objServer,
466 jsonPointerPath{std::string(jsonPointerPath)}, path{std::string(path)},
467 board](const boost::container::flat_map<std::string, JsonVariantType>&
468 data) {
469 nlohmann::json::json_pointer ptr(jsonPointerPath);
470 nlohmann::json& base = systemConfiguration[ptr];
471 auto findExposes = base.find("Exposes");
472
473 if (findExposes == base.end())
474 {
475 throw std::invalid_argument("Entity must have children.");
476 }
477
478 // this will throw invalid-argument to sdbusplus if invalid json
479 nlohmann::json newData{};
480 for (const auto& item : data)
481 {
482 nlohmann::json& newJson = newData[item.first];
483 std::visit(
484 [&newJson](auto&& val) {
485 newJson = std::forward<decltype(val)>(val);
486 },
487 item.second);
488 }
489
490 auto findName = newData.find("Name");
491 auto findType = newData.find("Type");
492 if (findName == newData.end() || findType == newData.end())
493 {
494 throw std::invalid_argument("AddObject missing Name or Type");
495 }
496 const std::string* type = findType->get_ptr<const std::string*>();
497 const std::string* name = findName->get_ptr<const std::string*>();
498 if (type == nullptr || name == nullptr)
499 {
500 throw std::invalid_argument("Type and Name must be a string.");
501 }
502
503 bool foundNull = false;
504 size_t lastIndex = 0;
505 // we add in the "exposes"
506 for (const auto& expose : *findExposes)
507 {
508 if (expose.is_null())
509 {
510 foundNull = true;
511 continue;
512 }
513
514 if (expose["Name"] == *name && expose["Type"] == *type)
515 {
516 throw std::invalid_argument(
517 "Field already in JSON, not adding");
518 }
519
520 if (foundNull)
521 {
522 continue;
523 }
524
525 lastIndex++;
526 }
527
528 std::ifstream schemaFile(std::string(schemaDirectory) + "/" +
529 boost::to_lower_copy(*type) + ".json");
530 // todo(james) we might want to also make a list of 'can add'
531 // interfaces but for now I think the assumption if there is a
532 // schema avaliable that it is allowed to update is fine
533 if (!schemaFile.good())
534 {
535 throw std::invalid_argument(
536 "No schema avaliable, cannot validate.");
537 }
538 nlohmann::json schema =
539 nlohmann::json::parse(schemaFile, nullptr, false, true);
540 if (schema.is_discarded())
541 {
542 std::cerr << "Schema not legal" << *type << ".json\n";
543 throw DBusInternalError();
544 }
545 if (!validateJson(schema, newData))
546 {
547 throw std::invalid_argument("Data does not match schema");
548 }
549 if (foundNull)
550 {
551 findExposes->at(lastIndex) = newData;
552 }
553 else
554 {
555 findExposes->push_back(newData);
556 }
557 if (!writeJsonFiles(systemConfiguration))
558 {
559 std::cerr << "Error writing json files\n";
560 throw DBusInternalError();
561 }
562 std::string dbusName = *name;
563
564 std::regex_replace(dbusName.begin(), dbusName.begin(),
565 dbusName.end(), illegalDbusMemberRegex, "_");
566
567 std::shared_ptr<sdbusplus::asio::dbus_interface> interface =
568 createInterface(objServer, path + "/" + dbusName,
569 "xyz.openbmc_project.Configuration." + *type,
570 board, true);
571 // permission is read-write, as since we just created it, must be
572 // runtime modifiable
573 populateInterfaceFromJson(
574 systemConfiguration,
575 jsonPointerPath + "/Exposes/" + std::to_string(lastIndex),
576 interface, newData, objServer,
577 sdbusplus::asio::PropertyPermission::readWrite);
578 });
579 tryIfaceInitialize(iface);
580 }
581
postToDbus(const nlohmann::json & newConfiguration,nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer)582 void postToDbus(const nlohmann::json& newConfiguration,
583 nlohmann::json& systemConfiguration,
584 sdbusplus::asio::object_server& objServer)
585
586 {
587 std::map<std::string, std::string> newBoards; // path -> name
588
589 // iterate through boards
590 for (const auto& [boardId, boardConfig] : newConfiguration.items())
591 {
592 std::string boardName = boardConfig["Name"];
593 std::string boardNameOrig = boardConfig["Name"];
594 std::string jsonPointerPath = "/" + boardId;
595 // loop through newConfiguration, but use values from system
596 // configuration to be able to modify via dbus later
597 auto boardValues = systemConfiguration[boardId];
598 auto findBoardType = boardValues.find("Type");
599 std::string boardType;
600 if (findBoardType != boardValues.end() &&
601 findBoardType->type() == nlohmann::json::value_t::string)
602 {
603 boardType = findBoardType->get<std::string>();
604 std::regex_replace(boardType.begin(), boardType.begin(),
605 boardType.end(), illegalDbusMemberRegex, "_");
606 }
607 else
608 {
609 std::cerr << "Unable to find type for " << boardName
610 << " reverting to Chassis.\n";
611 boardType = "Chassis";
612 }
613 std::string boardtypeLower = boost::algorithm::to_lower_copy(boardType);
614
615 std::regex_replace(boardName.begin(), boardName.begin(),
616 boardName.end(), illegalDbusMemberRegex, "_");
617 std::string boardPath = "/xyz/openbmc_project/inventory/system/";
618 boardPath += boardtypeLower;
619 boardPath += "/";
620 boardPath += boardName;
621
622 std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
623 createInterface(objServer, boardPath,
624 "xyz.openbmc_project.Inventory.Item", boardName);
625
626 std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
627 createInterface(objServer, boardPath,
628 "xyz.openbmc_project.Inventory.Item." + boardType,
629 boardNameOrig);
630
631 createAddObjectMethod(jsonPointerPath, boardPath, systemConfiguration,
632 objServer, boardNameOrig);
633
634 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
635 boardIface, boardValues, objServer);
636 jsonPointerPath += "/";
637 // iterate through board properties
638 for (const auto& [propName, propValue] : boardValues.items())
639 {
640 if (propValue.type() == nlohmann::json::value_t::object)
641 {
642 std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
643 createInterface(objServer, boardPath, propName,
644 boardNameOrig);
645
646 populateInterfaceFromJson(systemConfiguration,
647 jsonPointerPath + propName, iface,
648 propValue, objServer);
649 }
650 }
651
652 auto exposes = boardValues.find("Exposes");
653 if (exposes == boardValues.end())
654 {
655 continue;
656 }
657 // iterate through exposes
658 jsonPointerPath += "Exposes/";
659
660 // store the board level pointer so we can modify it on the way down
661 std::string jsonPointerPathBoard = jsonPointerPath;
662 size_t exposesIndex = -1;
663 for (auto& item : *exposes)
664 {
665 exposesIndex++;
666 jsonPointerPath = jsonPointerPathBoard;
667 jsonPointerPath += std::to_string(exposesIndex);
668
669 auto findName = item.find("Name");
670 if (findName == item.end())
671 {
672 std::cerr << "cannot find name in field " << item << "\n";
673 continue;
674 }
675 auto findStatus = item.find("Status");
676 // if status is not found it is assumed to be status = 'okay'
677 if (findStatus != item.end())
678 {
679 if (*findStatus == "disabled")
680 {
681 continue;
682 }
683 }
684 auto findType = item.find("Type");
685 std::string itemType;
686 if (findType != item.end())
687 {
688 itemType = findType->get<std::string>();
689 std::regex_replace(itemType.begin(), itemType.begin(),
690 itemType.end(), illegalDbusPathRegex, "_");
691 }
692 else
693 {
694 itemType = "unknown";
695 }
696 std::string itemName = findName->get<std::string>();
697 std::regex_replace(itemName.begin(), itemName.begin(),
698 itemName.end(), illegalDbusMemberRegex, "_");
699 std::string ifacePath = boardPath;
700 ifacePath += "/";
701 ifacePath += itemName;
702
703 if (itemType == "BMC")
704 {
705 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
706 createInterface(objServer, ifacePath,
707 "xyz.openbmc_project.Inventory.Item.Bmc",
708 boardNameOrig);
709 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
710 bmcIface, item, objServer,
711 getPermission(itemType));
712 }
713 else if (itemType == "System")
714 {
715 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
716 createInterface(objServer, ifacePath,
717 "xyz.openbmc_project.Inventory.Item.System",
718 boardNameOrig);
719 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
720 systemIface, item, objServer,
721 getPermission(itemType));
722 }
723
724 for (const auto& [name, config] : item.items())
725 {
726 jsonPointerPath = jsonPointerPathBoard;
727 jsonPointerPath.append(std::to_string(exposesIndex))
728 .append("/")
729 .append(name);
730 if (config.type() == nlohmann::json::value_t::object)
731 {
732 std::string ifaceName =
733 "xyz.openbmc_project.Configuration.";
734 ifaceName.append(itemType).append(".").append(name);
735
736 std::shared_ptr<sdbusplus::asio::dbus_interface>
737 objectIface = createInterface(objServer, ifacePath,
738 ifaceName, boardNameOrig);
739
740 populateInterfaceFromJson(
741 systemConfiguration, jsonPointerPath, objectIface,
742 config, objServer, getPermission(name));
743 }
744 else if (config.type() == nlohmann::json::value_t::array)
745 {
746 size_t index = 0;
747 if (config.empty())
748 {
749 continue;
750 }
751 bool isLegal = true;
752 auto type = config[0].type();
753 if (type != nlohmann::json::value_t::object)
754 {
755 continue;
756 }
757
758 // verify legal json
759 for (const auto& arrayItem : config)
760 {
761 if (arrayItem.type() != type)
762 {
763 isLegal = false;
764 break;
765 }
766 }
767 if (!isLegal)
768 {
769 std::cerr << "dbus format error" << config << "\n";
770 break;
771 }
772
773 for (auto& arrayItem : config)
774 {
775 std::string ifaceName =
776 "xyz.openbmc_project.Configuration.";
777 ifaceName.append(itemType).append(".").append(name);
778 ifaceName.append(std::to_string(index));
779
780 std::shared_ptr<sdbusplus::asio::dbus_interface>
781 objectIface = createInterface(
782 objServer, ifacePath, ifaceName, boardNameOrig);
783
784 populateInterfaceFromJson(
785 systemConfiguration,
786 jsonPointerPath + "/" + std::to_string(index),
787 objectIface, arrayItem, objServer,
788 getPermission(name));
789 index++;
790 }
791 }
792 }
793
794 std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
795 createInterface(objServer, ifacePath,
796 "xyz.openbmc_project.Configuration." + itemType,
797 boardNameOrig);
798
799 populateInterfaceFromJson(systemConfiguration, jsonPointerPath,
800 itemIface, item, objServer,
801 getPermission(itemType));
802
803 topology.addBoard(boardPath, boardType, boardNameOrig, item);
804 }
805
806 newBoards.emplace(boardPath, boardNameOrig);
807 }
808
809 for (const auto& [assocPath, assocPropValue] :
810 topology.getAssocs(newBoards))
811 {
812 auto findBoard = newBoards.find(assocPath);
813 if (findBoard == newBoards.end())
814 {
815 continue;
816 }
817
818 auto ifacePtr = createInterface(
819 objServer, assocPath, "xyz.openbmc_project.Association.Definitions",
820 findBoard->second);
821
822 ifacePtr->register_property("Associations", assocPropValue);
823 tryIfaceInitialize(ifacePtr);
824 }
825 }
826
827 // reads json files out of the filesystem
loadConfigurations(std::list<nlohmann::json> & configurations)828 bool loadConfigurations(std::list<nlohmann::json>& configurations)
829 {
830 // find configuration files
831 std::vector<std::filesystem::path> jsonPaths;
832 if (!findFiles(
833 std::vector<std::filesystem::path>{configurationDirectory,
834 hostConfigurationDirectory},
835 R"(.*\.json)", jsonPaths))
836 {
837 std::cerr << "Unable to find any configuration files in "
838 << configurationDirectory << "\n";
839 return false;
840 }
841
842 std::ifstream schemaStream(
843 std::string(schemaDirectory) + "/" + globalSchema);
844 if (!schemaStream.good())
845 {
846 std::cerr
847 << "Cannot open schema file, cannot validate JSON, exiting\n\n";
848 std::exit(EXIT_FAILURE);
849 return false;
850 }
851 nlohmann::json schema =
852 nlohmann::json::parse(schemaStream, nullptr, false, true);
853 if (schema.is_discarded())
854 {
855 std::cerr
856 << "Illegal schema file detected, cannot validate JSON, exiting\n";
857 std::exit(EXIT_FAILURE);
858 return false;
859 }
860
861 for (auto& jsonPath : jsonPaths)
862 {
863 std::ifstream jsonStream(jsonPath.c_str());
864 if (!jsonStream.good())
865 {
866 std::cerr << "unable to open " << jsonPath.string() << "\n";
867 continue;
868 }
869 auto data = nlohmann::json::parse(jsonStream, nullptr, false, true);
870 if (data.is_discarded())
871 {
872 std::cerr << "syntax error in " << jsonPath.string() << "\n";
873 continue;
874 }
875 /*
876 * todo(james): reenable this once less things are in flight
877 *
878 if (!validateJson(schema, data))
879 {
880 std::cerr << "Error validating " << jsonPath.string() << "\n";
881 continue;
882 }
883 */
884
885 if (data.type() == nlohmann::json::value_t::array)
886 {
887 for (auto& d : data)
888 {
889 configurations.emplace_back(d);
890 }
891 }
892 else
893 {
894 configurations.emplace_back(data);
895 }
896 }
897 return true;
898 }
899
deviceRequiresPowerOn(const nlohmann::json & entity)900 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
901 {
902 auto powerState = entity.find("PowerState");
903 if (powerState == entity.end())
904 {
905 return false;
906 }
907
908 const auto* ptr = powerState->get_ptr<const std::string*>();
909 if (ptr == nullptr)
910 {
911 return false;
912 }
913
914 return *ptr == "On" || *ptr == "BiosPost";
915 }
916
pruneDevice(const nlohmann::json & systemConfiguration,const bool powerOff,const bool scannedPowerOff,const std::string & name,const nlohmann::json & device)917 static void pruneDevice(const nlohmann::json& systemConfiguration,
918 const bool powerOff, const bool scannedPowerOff,
919 const std::string& name, const nlohmann::json& device)
920 {
921 if (systemConfiguration.contains(name))
922 {
923 return;
924 }
925
926 if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
927 {
928 return;
929 }
930
931 logDeviceRemoved(device);
932 }
933
startRemovedTimer(boost::asio::steady_timer & timer,nlohmann::json & systemConfiguration)934 void startRemovedTimer(boost::asio::steady_timer& timer,
935 nlohmann::json& systemConfiguration)
936 {
937 static bool scannedPowerOff = false;
938 static bool scannedPowerOn = false;
939
940 if (systemConfiguration.empty() || lastJson.empty())
941 {
942 return; // not ready yet
943 }
944 if (scannedPowerOn)
945 {
946 return;
947 }
948
949 if (!isPowerOn() && scannedPowerOff)
950 {
951 return;
952 }
953
954 timer.expires_after(std::chrono::seconds(10));
955 timer.async_wait(
956 [&systemConfiguration](const boost::system::error_code& ec) {
957 if (ec == boost::asio::error::operation_aborted)
958 {
959 return;
960 }
961
962 bool powerOff = !isPowerOn();
963 for (const auto& [name, device] : lastJson.items())
964 {
965 pruneDevice(systemConfiguration, powerOff, scannedPowerOff,
966 name, device);
967 }
968
969 scannedPowerOff = true;
970 if (!powerOff)
971 {
972 scannedPowerOn = true;
973 }
974 });
975 }
976
977 static std::vector<std::weak_ptr<sdbusplus::asio::dbus_interface>>&
getDeviceInterfaces(const nlohmann::json & device)978 getDeviceInterfaces(const nlohmann::json& device)
979 {
980 return inventory[device["Name"].get<std::string>()];
981 }
982
pruneConfiguration(nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer,bool powerOff,const std::string & name,const nlohmann::json & device)983 static void pruneConfiguration(nlohmann::json& systemConfiguration,
984 sdbusplus::asio::object_server& objServer,
985 bool powerOff, const std::string& name,
986 const nlohmann::json& device)
987 {
988 if (powerOff && deviceRequiresPowerOn(device))
989 {
990 // power not on yet, don't know if it's there or not
991 return;
992 }
993
994 auto& ifaces = getDeviceInterfaces(device);
995 for (auto& iface : ifaces)
996 {
997 auto sharedPtr = iface.lock();
998 if (!!sharedPtr)
999 {
1000 objServer.remove_interface(sharedPtr);
1001 }
1002 }
1003
1004 ifaces.clear();
1005 systemConfiguration.erase(name);
1006 topology.remove(device["Name"].get<std::string>());
1007 logDeviceRemoved(device);
1008 }
1009
deriveNewConfiguration(const nlohmann::json & oldConfiguration,nlohmann::json & newConfiguration)1010 static void deriveNewConfiguration(const nlohmann::json& oldConfiguration,
1011 nlohmann::json& newConfiguration)
1012 {
1013 for (auto it = newConfiguration.begin(); it != newConfiguration.end();)
1014 {
1015 auto findKey = oldConfiguration.find(it.key());
1016 if (findKey != oldConfiguration.end())
1017 {
1018 it = newConfiguration.erase(it);
1019 }
1020 else
1021 {
1022 it++;
1023 }
1024 }
1025 }
1026
publishNewConfiguration(const size_t & instance,const size_t count,boost::asio::steady_timer & timer,nlohmann::json & systemConfiguration,const nlohmann::json newConfiguration,sdbusplus::asio::object_server & objServer)1027 static void publishNewConfiguration(
1028 const size_t& instance, const size_t count,
1029 boost::asio::steady_timer& timer, nlohmann::json& systemConfiguration,
1030 // Gerrit discussion:
1031 // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
1032 //
1033 // Discord discussion:
1034 // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
1035 //
1036 // NOLINTNEXTLINE(performance-unnecessary-value-param)
1037 const nlohmann::json newConfiguration,
1038 sdbusplus::asio::object_server& objServer)
1039 {
1040 loadOverlays(newConfiguration);
1041
1042 boost::asio::post(io, [systemConfiguration]() {
1043 if (!writeJsonFiles(systemConfiguration))
1044 {
1045 std::cerr << "Error writing json files\n";
1046 }
1047 });
1048
1049 boost::asio::post(io, [&instance, count, &timer, newConfiguration,
1050 &systemConfiguration, &objServer]() {
1051 postToDbus(newConfiguration, systemConfiguration, objServer);
1052 if (count == instance)
1053 {
1054 startRemovedTimer(timer, systemConfiguration);
1055 }
1056 });
1057 }
1058
1059 // main properties changed entry
propertiesChangedCallback(nlohmann::json & systemConfiguration,sdbusplus::asio::object_server & objServer)1060 void propertiesChangedCallback(nlohmann::json& systemConfiguration,
1061 sdbusplus::asio::object_server& objServer)
1062 {
1063 static bool inProgress = false;
1064 static boost::asio::steady_timer timer(io);
1065 static size_t instance = 0;
1066 instance++;
1067 size_t count = instance;
1068
1069 timer.expires_after(std::chrono::milliseconds(500));
1070
1071 // setup an async wait as we normally get flooded with new requests
1072 timer.async_wait([&systemConfiguration, &objServer,
1073 count](const boost::system::error_code& ec) {
1074 if (ec == boost::asio::error::operation_aborted)
1075 {
1076 // we were cancelled
1077 return;
1078 }
1079 if (ec)
1080 {
1081 std::cerr << "async wait error " << ec << "\n";
1082 return;
1083 }
1084
1085 if (inProgress)
1086 {
1087 propertiesChangedCallback(systemConfiguration, objServer);
1088 return;
1089 }
1090 inProgress = true;
1091
1092 nlohmann::json oldConfiguration = systemConfiguration;
1093 auto missingConfigurations = std::make_shared<nlohmann::json>();
1094 *missingConfigurations = systemConfiguration;
1095
1096 std::list<nlohmann::json> configurations;
1097 if (!loadConfigurations(configurations))
1098 {
1099 std::cerr << "Could not load configurations\n";
1100 inProgress = false;
1101 return;
1102 }
1103
1104 auto perfScan = std::make_shared<PerformScan>(
1105 systemConfiguration, *missingConfigurations, configurations,
1106 objServer,
1107 [&systemConfiguration, &objServer, count, oldConfiguration,
1108 missingConfigurations]() {
1109 // this is something that since ac has been applied to the bmc
1110 // we saw, and we no longer see it
1111 bool powerOff = !isPowerOn();
1112 for (const auto& [name, device] :
1113 missingConfigurations->items())
1114 {
1115 pruneConfiguration(systemConfiguration, objServer, powerOff,
1116 name, device);
1117 }
1118
1119 nlohmann::json newConfiguration = systemConfiguration;
1120
1121 deriveNewConfiguration(oldConfiguration, newConfiguration);
1122
1123 for (const auto& [_, device] : newConfiguration.items())
1124 {
1125 logDeviceAdded(device);
1126 }
1127
1128 inProgress = false;
1129
1130 boost::asio::post(
1131 io, std::bind_front(
1132 publishNewConfiguration, std::ref(instance), count,
1133 std::ref(timer), std::ref(systemConfiguration),
1134 newConfiguration, std::ref(objServer)));
1135 });
1136 perfScan->run();
1137 });
1138 }
1139
1140 // Extract the D-Bus interfaces to probe from the JSON config files.
getProbeInterfaces()1141 static std::set<std::string> getProbeInterfaces()
1142 {
1143 std::set<std::string> interfaces;
1144 std::list<nlohmann::json> configurations;
1145 if (!loadConfigurations(configurations))
1146 {
1147 return interfaces;
1148 }
1149
1150 for (auto it = configurations.begin(); it != configurations.end();)
1151 {
1152 auto findProbe = it->find("Probe");
1153 if (findProbe == it->end())
1154 {
1155 std::cerr << "configuration file missing probe:\n " << *it << "\n";
1156 it++;
1157 continue;
1158 }
1159
1160 nlohmann::json probeCommand;
1161 if ((*findProbe).type() != nlohmann::json::value_t::array)
1162 {
1163 probeCommand = nlohmann::json::array();
1164 probeCommand.push_back(*findProbe);
1165 }
1166 else
1167 {
1168 probeCommand = *findProbe;
1169 }
1170
1171 for (const nlohmann::json& probeJson : probeCommand)
1172 {
1173 const std::string* probe = probeJson.get_ptr<const std::string*>();
1174 if (probe == nullptr)
1175 {
1176 std::cerr << "Probe statement wasn't a string, can't parse";
1177 continue;
1178 }
1179 // Skip it if the probe cmd doesn't contain an interface.
1180 if (findProbeType(*probe))
1181 {
1182 continue;
1183 }
1184
1185 // syntax requires probe before first open brace
1186 auto findStart = probe->find('(');
1187 if (findStart != std::string::npos)
1188 {
1189 std::string interface = probe->substr(0, findStart);
1190 interfaces.emplace(interface);
1191 }
1192 }
1193 it++;
1194 }
1195
1196 return interfaces;
1197 }
1198
1199 // Check if InterfacesAdded payload contains an iface that needs probing.
iaContainsProbeInterface(sdbusplus::message_t & msg,const std::set<std::string> & probeInterfaces)1200 static bool iaContainsProbeInterface(
1201 sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
1202 {
1203 sdbusplus::message::object_path path;
1204 DBusObject interfaces;
1205 std::set<std::string> interfaceSet;
1206 std::set<std::string> intersect;
1207
1208 msg.read(path, interfaces);
1209
1210 std::for_each(interfaces.begin(), interfaces.end(),
1211 [&interfaceSet](const auto& iface) {
1212 interfaceSet.insert(iface.first);
1213 });
1214
1215 std::set_intersection(interfaceSet.begin(), interfaceSet.end(),
1216 probeInterfaces.begin(), probeInterfaces.end(),
1217 std::inserter(intersect, intersect.end()));
1218 return !intersect.empty();
1219 }
1220
1221 // Check if InterfacesRemoved payload contains an iface that needs probing.
irContainsProbeInterface(sdbusplus::message_t & msg,const std::set<std::string> & probeInterfaces)1222 static bool irContainsProbeInterface(
1223 sdbusplus::message_t& msg, const std::set<std::string>& probeInterfaces)
1224 {
1225 sdbusplus::message::object_path path;
1226 std::set<std::string> interfaces;
1227 std::set<std::string> intersect;
1228
1229 msg.read(path, interfaces);
1230
1231 std::set_intersection(interfaces.begin(), interfaces.end(),
1232 probeInterfaces.begin(), probeInterfaces.end(),
1233 std::inserter(intersect, intersect.end()));
1234 return !intersect.empty();
1235 }
1236
main()1237 int main()
1238 {
1239 // setup connection to dbus
1240 systemBus = std::make_shared<sdbusplus::asio::connection>(io);
1241 systemBus->request_name("xyz.openbmc_project.EntityManager");
1242
1243 // The EntityManager object itself doesn't expose any properties.
1244 // No need to set up ObjectManager for the |EntityManager| object.
1245 sdbusplus::asio::object_server objServer(systemBus, /*skipManager=*/true);
1246
1247 // All other objects that EntityManager currently support are under the
1248 // inventory subtree.
1249 // See the discussion at
1250 // https://discord.com/channels/775381525260664832/1018929092009144380
1251 objServer.add_manager("/xyz/openbmc_project/inventory");
1252
1253 std::shared_ptr<sdbusplus::asio::dbus_interface> entityIface =
1254 objServer.add_interface("/xyz/openbmc_project/EntityManager",
1255 "xyz.openbmc_project.EntityManager");
1256
1257 // to keep reference to the match / filter objects so they don't get
1258 // destroyed
1259
1260 nlohmann::json systemConfiguration = nlohmann::json::object();
1261
1262 std::set<std::string> probeInterfaces = getProbeInterfaces();
1263
1264 // We need a poke from DBus for static providers that create all their
1265 // objects prior to claiming a well-known name, and thus don't emit any
1266 // org.freedesktop.DBus.Properties signals. Similarly if a process exits
1267 // for any reason, expected or otherwise, we'll need a poke to remove
1268 // entities from DBus.
1269 sdbusplus::bus::match_t nameOwnerChangedMatch(
1270 static_cast<sdbusplus::bus_t&>(*systemBus),
1271 sdbusplus::bus::match::rules::nameOwnerChanged(),
1272 [&](sdbusplus::message_t& m) {
1273 auto [name, oldOwner,
1274 newOwner] = m.unpack<std::string, std::string, std::string>();
1275
1276 if (name.starts_with(':'))
1277 {
1278 // We should do nothing with unique-name connections.
1279 return;
1280 }
1281
1282 propertiesChangedCallback(systemConfiguration, objServer);
1283 });
1284 // We also need a poke from DBus when new interfaces are created or
1285 // destroyed.
1286 sdbusplus::bus::match_t interfacesAddedMatch(
1287 static_cast<sdbusplus::bus_t&>(*systemBus),
1288 sdbusplus::bus::match::rules::interfacesAdded(),
1289 [&](sdbusplus::message_t& msg) {
1290 if (iaContainsProbeInterface(msg, probeInterfaces))
1291 {
1292 propertiesChangedCallback(systemConfiguration, objServer);
1293 }
1294 });
1295 sdbusplus::bus::match_t interfacesRemovedMatch(
1296 static_cast<sdbusplus::bus_t&>(*systemBus),
1297 sdbusplus::bus::match::rules::interfacesRemoved(),
1298 [&](sdbusplus::message_t& msg) {
1299 if (irContainsProbeInterface(msg, probeInterfaces))
1300 {
1301 propertiesChangedCallback(systemConfiguration, objServer);
1302 }
1303 });
1304
1305 boost::asio::post(io, [&]() {
1306 propertiesChangedCallback(systemConfiguration, objServer);
1307 });
1308
1309 entityIface->register_method("ReScan", [&]() {
1310 propertiesChangedCallback(systemConfiguration, objServer);
1311 });
1312 tryIfaceInitialize(entityIface);
1313
1314 if (fwVersionIsSame())
1315 {
1316 if (std::filesystem::is_regular_file(currentConfiguration))
1317 {
1318 // this file could just be deleted, but it's nice for debug
1319 std::filesystem::create_directory(tempConfigDir);
1320 std::filesystem::remove(lastConfiguration);
1321 std::filesystem::copy(currentConfiguration, lastConfiguration);
1322 std::filesystem::remove(currentConfiguration);
1323
1324 std::ifstream jsonStream(lastConfiguration);
1325 if (jsonStream.good())
1326 {
1327 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
1328 if (data.is_discarded())
1329 {
1330 std::cerr
1331 << "syntax error in " << lastConfiguration << "\n";
1332 }
1333 else
1334 {
1335 lastJson = std::move(data);
1336 }
1337 }
1338 else
1339 {
1340 std::cerr << "unable to open " << lastConfiguration << "\n";
1341 }
1342 }
1343 }
1344 else
1345 {
1346 // not an error, just logging at this level to make it in the journal
1347 std::cerr << "Clearing previous configuration\n";
1348 std::filesystem::remove(currentConfiguration);
1349 }
1350
1351 // some boards only show up after power is on, we want to not say they are
1352 // removed until the same state happens
1353 setupPowerMatch(systemBus);
1354
1355 io.run();
1356
1357 return 0;
1358 }
1359