Add probe FOUND() logic in tlbmc.

Support FOUND('<something>') syntax in tlbmc.
The process step:
1. Store the configs with FOUND probe
2. Process the other probe e.g. IpmiFru
3. Use topological sort to avoid recursion and find if the FOUND target exists or not.

#tlbmc

Startblock:
  * has LGTM from tomtung
  * and then
  * add reviewer c-readability-approvers
  * add reviewer nanzhou
PiperOrigin-RevId: 783370731
Change-Id: I32560a343dd08fc940df27ce60dc977f7c36b23f
diff --git a/tlbmc/configs/entity_config_json_impl.cc b/tlbmc/configs/entity_config_json_impl.cc
index bd0fcff..e452dc0 100644
--- a/tlbmc/configs/entity_config_json_impl.cc
+++ b/tlbmc/configs/entity_config_json_impl.cc
@@ -30,6 +30,7 @@
 #include "absl/strings/str_join.h"
 #include "absl/strings/str_replace.h"
 #include "absl/strings/string_view.h"
+#include "absl/strings/strip.h"
 #include "absl/strings/substitute.h"
 #include "absl/time/time.h"
 #include "absl/types/span.h"
@@ -454,9 +455,101 @@
           RE2::FullMatch(find_fru_field->second, *probe_field));
 }
 
+// Probes if the Ipmi Fru matches and updates the fru table and probed
+// config map.
+absl::Status ProbeFru(nlohmann::json& ipmi_fru_object,
+                      const nlohmann::json& config, absl::string_view name,
+                      FruTable& fru_table, ProbedConfigMap& probed_config_map) {
+  if (!ipmi_fru_object.is_array()) {
+    LOG(INFO) << "IpmiFru field is not an array, converting to array";
+    ipmi_fru_object = nlohmann::json::array({ipmi_fru_object});
+  }
+  for (const auto& ipmi_fru : ipmi_fru_object) {
+    // Get probe fields.
+    const auto* board_product_name =
+        GetValueAsString(ipmi_fru, "BOARD_PRODUCT_NAME");
+    const auto* product_product_name =
+        GetValueAsString(ipmi_fru, "PRODUCT_PRODUCT_NAME");
+    const auto* bus = GetValueAsUint(ipmi_fru, "BUS");
+    const auto* address = GetValueAsUint(ipmi_fru, "ADDRESS");
+    const auto* part_number = GetValueAsString(ipmi_fru, "BOARD_PART_NUMBER");
+    const auto* board_info_am2 = GetValueAsString(ipmi_fru, "BOARD_INFO_AM2");
+    const auto* product_info_am2 =
+        GetValueAsString(ipmi_fru, "PRODUCT_INFO_AM2");
+    if (board_product_name == nullptr && product_product_name == nullptr &&
+        bus == nullptr && address == nullptr && part_number == nullptr &&
+        board_info_am2 == nullptr && product_info_am2 == nullptr) {
+      return absl::InvalidArgumentError(
+          "Invalid config: IpmiFru field is not a valid probe");
+    }
+
+    // Check if the FRU matches the probe.
+    for (const auto& [key, fru] : fru_table.key_to_fru()) {
+      bool i2c_info_matches_or_null = false;
+      if ((bus == nullptr && address == nullptr) ||
+          (bus != nullptr && *bus == fru.i2c_info().bus() &&
+           address != nullptr && *address == fru.i2c_info().address())) {
+        i2c_info_matches_or_null = true;
+      }
+
+      // Check if any of the FRU fields match the probe.
+      bool board_product_name_matches_or_null =
+          CheckProbeFieldMatch(board_product_name, fru, "BOARD_PRODUCT_NAME");
+      bool product_product_name_matches_or_null = CheckProbeFieldMatch(
+          product_product_name, fru, "PRODUCT_PRODUCT_NAME");
+      bool part_number_matches_or_null =
+          CheckProbeFieldMatch(part_number, fru, "BOARD_PART_NUMBER");
+      bool board_info_am2_matches_or_null =
+          CheckProbeFieldMatch(board_info_am2, fru, "BOARD_INFO_AM2");
+      bool product_info_am2_matches_or_null =
+          CheckProbeFieldMatch(product_info_am2, fru, "PRODUCT_INFO_AM2");
+
+      // If the FRU doesn't match the probe, continue to the next FRU.
+      if (!i2c_info_matches_or_null || !board_product_name_matches_or_null ||
+          !product_product_name_matches_or_null ||
+          !part_number_matches_or_null || !board_info_am2_matches_or_null ||
+          !product_info_am2_matches_or_null) {
+        continue;
+      }
+
+      // This condition will check that
+      // 1. Only configs containing in $index can have multiple Fru probe true
+      // 2. A Fru cannot match multiple probes within one config
+      // 3. Multiple configs cannot probe true with the same name
+      if (probed_config_map.contains(name) &&
+          !absl::StrContains(name, "$index")) {
+        return absl::InvalidArgumentError(
+            absl::StrCat("Invalid config: Frus match the same config multiple"
+                         " times but config name does not contain $index : ",
+                         name));
+      }
+      // If we reach here, the FRU matches the probe and is unique.
+      auto& probed_data = probed_config_map[name];
+
+      // If the key is in the format of "bus:address", we will parse the bus
+      // and address and use them to construct the FruKey object. Otherwise,
+      // default to the string representation of the FruKey.
+      probed_data.fru_keys.push_back(FruKey(key));
+      probed_data.config = config;
+      LOG(INFO) << "Matched config: " << name << " with FRU: " << key;
+    }
+    // Sort the found FRU keys for each config.
+    if (auto probed_data_it = probed_config_map.find(name);
+        probed_data_it != probed_config_map.end()) {
+      std::sort(probed_data_it->second.fru_keys.begin(),
+                probed_data_it->second.fru_keys.end());
+    }
+  }
+  return absl::OkStatus();
+}
+
+// Processes probe configs and returns a map of them.
 absl::StatusOr<ProbedConfigMap> ProcessConfigs(
     const std::vector<nlohmann::json>& config_list, FruTable& fru_table) {
   ProbedConfigMap probed_config_map;
+  // Map to store the temporary config map for configs that have FOUND probe.
+  absl::flat_hash_map<std::string, std::vector<ProbedConfigData>>
+      pending_config_name_to_probed_configs;
   for (const auto& config : config_list) {
     absl::StatusOr<nlohmann::json> probe_result =
         GetObject(config, kProbeKeyword);
@@ -468,28 +561,41 @@
     ECCLESIA_ASSIGN_OR_RETURN(std::string name, ParseConfigName(config));
     name = absl::StrReplaceAll(name, {{" ", "_"}});
 
-    const std::string* true_str = probe_result->get_ptr<const std::string*>();
-    bool true_value = true_str != nullptr && *true_str == "TRUE";
-    // Check if the probe is a true value.
-    if (true_value) {
-      if (probed_config_map.contains(name)) {
-        return absl::InvalidArgumentError(
-            absl::StrCat("Invalid config: Multiple configs with TRUE probe have"
-                         " the same name: ",
-                         name));
+    const std::string* probe_str = probe_result->get_ptr<const std::string*>();
+    if (probe_str != nullptr) {
+      std::string_view probe_str_view(*probe_str);
+      if (probe_str_view == "TRUE") {
+        if (probed_config_map.contains(name)) {
+          return absl::InvalidArgumentError(
+              absl::StrCat("Invalid config: Multiple configs with TRUE probe"
+                           " have the same name: ",
+                           name));
+        }
+        auto& probed_data = probed_config_map[name];
+        probed_data.fru_keys.push_back(FruKey(name));
+        probed_data.config = config;
+        Fru entity_object;
+        entity_object.mutable_attributes()->set_key(name);
+        entity_object.mutable_attributes()->set_status(STATUS_READY);
+        fru_table.mutable_key_to_fru()->insert({name, entity_object});
+        continue;
       }
-      auto& probed_data = probed_config_map[name];
-      probed_data.fru_keys.push_back(FruKey(name));
-      probed_data.config = config;
-      Fru entity_object;
-      entity_object.mutable_attributes()->set_key(name);
-      entity_object.mutable_attributes()->set_status(STATUS_READY);
-      fru_table.mutable_key_to_fru()->insert({name, entity_object});
-      continue;
-    }
-    bool skip_value = true_str != nullptr && *true_str == "FALSE";
-    if (skip_value) {
-      continue;
+      if (probe_str_view == "FALSE") {
+        continue;
+      }
+      // Store the config in the temporary map with key as FOUND probe
+      // and handle the config later.
+      if (absl::StartsWith(probe_str_view, "FOUND('") &&
+          absl::EndsWith(probe_str_view, "')")) {
+        absl::string_view found_config_name = absl::StripSuffix(
+            absl::StripPrefix(probe_str_view, "FOUND('"), "')");
+        ProbedConfigData probed_data;
+        probed_data.fru_keys.push_back(FruKey(name));
+        probed_data.config = config;
+        pending_config_name_to_probed_configs[found_config_name].push_back(
+            probed_data);
+        continue;
+      }
     }
     nlohmann::json probe = *probe_result;
     // Perform probe.
@@ -500,85 +606,48 @@
           "Invalid config: IpmiFru field is not a valid probe");
     }
     auto ipmi_fru_object = ipmi_fru_it->get<nlohmann::json>();
-    // Wrap single IpmiFru object in array to simplify logic below.
-    if (!ipmi_fru_object.is_array()) {
-      ipmi_fru_object = nlohmann::json::array({ipmi_fru_object});
+    absl::Status probe_fru_status =
+        ProbeFru(ipmi_fru_object, config, name, fru_table, probed_config_map);
+    if (!probe_fru_status.ok()) {
+      return probe_fru_status;
     }
-    for (const auto& ipmi_fru : ipmi_fru_object) {
-      // Get probe fields.
-      const auto* board_product_name =
-          GetValueAsString(ipmi_fru, "BOARD_PRODUCT_NAME");
-      const auto* product_product_name =
-          GetValueAsString(ipmi_fru, "PRODUCT_PRODUCT_NAME");
-      const auto* bus = GetValueAsUint(ipmi_fru, "BUS");
-      const auto* address = GetValueAsUint(ipmi_fru, "ADDRESS");
-      const auto* part_number = GetValueAsString(ipmi_fru, "BOARD_PART_NUMBER");
-      const auto* board_info_am2 = GetValueAsString(ipmi_fru, "BOARD_INFO_AM2");
-      const auto* product_info_am2 =
-          GetValueAsString(ipmi_fru, "PRODUCT_INFO_AM2");
-      if (board_product_name == nullptr && product_product_name == nullptr &&
-          bus == nullptr && address == nullptr && part_number == nullptr &&
-          board_info_am2 == nullptr && product_info_am2 == nullptr) {
-        return absl::InvalidArgumentError(
-            "Invalid config: IpmiFru field is not a valid probe");
-      }
+  }
 
-      // Check if the FRU matches the probe.
-      for (const auto& [key, fru] : fru_table.key_to_fru()) {
-        bool i2c_info_matches_or_null = false;
-        if ((bus == nullptr && address == nullptr) ||
-            (bus != nullptr && *bus == fru.i2c_info().bus() &&
-             address != nullptr && *address == fru.i2c_info().address())) {
-          i2c_info_matches_or_null = true;
-        }
-
-        // Check if any of the FRU fields match the probe.
-        bool board_product_name_matches_or_null =
-            CheckProbeFieldMatch(board_product_name, fru, "BOARD_PRODUCT_NAME");
-        bool product_product_name_matches_or_null = CheckProbeFieldMatch(
-            product_product_name, fru, "PRODUCT_PRODUCT_NAME");
-        bool part_number_matches_or_null =
-            CheckProbeFieldMatch(part_number, fru, "BOARD_PART_NUMBER");
-        bool board_info_am2_matches_or_null =
-            CheckProbeFieldMatch(board_info_am2, fru, "BOARD_INFO_AM2");
-        bool product_info_am2_matches_or_null =
-            CheckProbeFieldMatch(product_info_am2, fru, "PRODUCT_INFO_AM2");
-
-        // If the FRU doesn't match the probe, continue to the next FRU.
-        if (!i2c_info_matches_or_null || !board_product_name_matches_or_null ||
-            !product_product_name_matches_or_null ||
-            !part_number_matches_or_null || !board_info_am2_matches_or_null ||
-            !product_info_am2_matches_or_null) {
-          continue;
-        }
-
-        // This condition will check that
-        // 1. Only configs containing in $index can have multiple Fru probe true
-        // 2. A Fru cannot match multiple probes within one config
-        // 3. Multiple configs cannot probe true with the same name
-        if (probed_config_map.contains(name) &&
+  // Post-process the configs with FOUND probe.
+  // Using topological sort (bfs) to find all the configs which FOUND probe
+  // rule is fulfilled. This is to avoid the recursion approach in
+  // https://github.com/openbmc/entity-manager/blob/7f51d32fb69f3bb6c4eabf685a27b57f345445cb/src/entity_manager/perform_scan.cpp#L582
+  std::queue<std::string> found_target_candidates;
+  for (const auto& [name, _] : probed_config_map) {
+    if (pending_config_name_to_probed_configs.find(name) !=
+        pending_config_name_to_probed_configs.end()) {
+      found_target_candidates.push(name);
+    }
+  }
+  while (!found_target_candidates.empty()) {
+    const std::string name = found_target_candidates.front();
+    found_target_candidates.pop();
+    auto found_config_it = pending_config_name_to_probed_configs.find(name);
+    if (found_config_it != pending_config_name_to_probed_configs.end()) {
+      for (const auto& found_config_data : found_config_it->second) {
+        const std::string config_name =
+            found_config_data.fru_keys[0].ToString();
+        if (probed_config_map.contains(config_name) &&
             !absl::StrContains(name, "$index")) {
           return absl::InvalidArgumentError(
-              absl::StrCat("Invalid config: Frus match the same config multiple"
-                           " times but config name does not contain $index : ",
-                           name));
+              absl::StrCat("Invalid config: Multiple configs with FOUND probe"
+                           " have the same name: ",
+                           config_name));
         }
-        // If we reach here, the FRU matches the probe and is unique.
-        auto& probed_data = probed_config_map[name];
-
-        // If the key is in the format of "bus:address", we will parse the bus
-        // and address and use them to construct the FruKey object. Otherwise,
-        // default to the string representation of the FruKey.
-        probed_data.fru_keys.push_back(FruKey(key));
-        probed_data.config = config;
-        LOG(INFO) << "Matched config: " << name << " with FRU: " << key;
+        found_target_candidates.push(config_name);
+        probed_config_map[config_name] = found_config_data;
+        Fru entity_object;
+        entity_object.mutable_attributes()->set_key(config_name);
+        entity_object.mutable_attributes()->set_status(STATUS_READY);
+        fru_table.mutable_key_to_fru()->insert({config_name, entity_object});
+        LOG(INFO) << "FOUND " << name << ", appending config " << config_name;
       }
-      // Sort the found FRU keys for each config.
-      if (auto probed_data_it = probed_config_map.find(name);
-          probed_data_it != probed_config_map.end()) {
-        std::sort(probed_data_it->second.fru_keys.begin(),
-                  probed_data_it->second.fru_keys.end());
-      }
+      pending_config_name_to_probed_configs.erase(found_config_it);
     }
   }
   return probed_config_map;