| #include "config.h" |
| |
| #include "common_utility.hpp" |
| #include "defines.hpp" |
| #include "ibm_vpd_utils.hpp" |
| #include "ipz_parser.hpp" |
| #include "keyword_vpd_parser.hpp" |
| #include "memory_vpd_parser.hpp" |
| #include "parser_factory.hpp" |
| #include "vpd_exceptions.hpp" |
| |
| #include <assert.h> |
| #include <ctype.h> |
| |
| #include <CLI/CLI.hpp> |
| #include <algorithm> |
| #include <boost/algorithm/string.hpp> |
| #include <cstdarg> |
| #include <exception> |
| #include <filesystem> |
| #include <fstream> |
| #include <gpiod.hpp> |
| #include <iostream> |
| #include <iterator> |
| #include <nlohmann/json.hpp> |
| #include <phosphor-logging/log.hpp> |
| #include <regex> |
| |
| using namespace std; |
| using namespace openpower::vpd; |
| using namespace CLI; |
| using namespace vpd::keyword::parser; |
| using namespace openpower::vpd::constants; |
| namespace fs = filesystem; |
| using json = nlohmann::json; |
| using namespace openpower::vpd::parser::factory; |
| using namespace openpower::vpd::inventory; |
| using namespace openpower::vpd::memory::parser; |
| using namespace openpower::vpd::parser::interface; |
| using namespace openpower::vpd::exceptions; |
| using namespace phosphor::logging; |
| |
| /** |
| * @brief Returns the BMC state |
| */ |
| static auto getBMCState() |
| { |
| std::string bmcState; |
| try |
| { |
| auto bus = sdbusplus::bus::new_default(); |
| auto properties = bus.new_method_call( |
| "xyz.openbmc_project.State.BMC", "/xyz/openbmc_project/state/bmc0", |
| "org.freedesktop.DBus.Properties", "Get"); |
| properties.append("xyz.openbmc_project.State.BMC"); |
| properties.append("CurrentBMCState"); |
| auto result = bus.call(properties); |
| std::variant<std::string> val; |
| result.read(val); |
| if (auto pVal = std::get_if<std::string>(&val)) |
| { |
| bmcState = *pVal; |
| } |
| } |
| catch (const sdbusplus::exception::SdBusError& e) |
| { |
| // Ignore any error |
| std::cerr << "Failed to get BMC state: " << e.what() << "\n"; |
| } |
| return bmcState; |
| } |
| |
| /** |
| * @brief Check if the FRU is in the cache |
| * |
| * Checks if the FRU associated with the supplied D-Bus object path is already |
| * on D-Bus. This can be used to test if a VPD collection is required for this |
| * FRU. It uses the "xyz.openbmc_project.Inventory.Item, Present" property to |
| * determine the presence of a FRU in the cache. |
| * |
| * @param objectPath - The D-Bus object path without the PIM prefix. |
| * @return true if the object exists on D-Bus, false otherwise. |
| */ |
| static auto isFruInVpdCache(const std::string& objectPath) |
| { |
| try |
| { |
| auto bus = sdbusplus::bus::new_default(); |
| auto invPath = std::string{pimPath} + objectPath; |
| auto props = bus.new_method_call( |
| "xyz.openbmc_project.Inventory.Manager", invPath.c_str(), |
| "org.freedesktop.DBus.Properties", "Get"); |
| props.append("xyz.openbmc_project.Inventory.Item"); |
| props.append("Present"); |
| auto result = bus.call(props); |
| std::variant<bool> present; |
| result.read(present); |
| if (auto pVal = std::get_if<bool>(&present)) |
| { |
| return *pVal; |
| } |
| return false; |
| } |
| catch (const sdbusplus::exception::SdBusError& e) |
| { |
| std::cout << "FRU: " << objectPath << " not in D-Bus\n"; |
| // Assume not present in case of an error |
| return false; |
| } |
| } |
| |
| /** |
| * @brief Check if VPD recollection is needed for the given EEPROM |
| * |
| * Not all FRUs can be swapped at BMC ready state. This function does the |
| * following: |
| * -- Check if the FRU is marked as "pluggableAtStandby" OR |
| * "concurrentlyMaintainable". If so, return true. |
| * -- Check if we are at BMC NotReady state. If we are, then return true. |
| * -- Else check if the FRU is not present in the VPD cache (to cover for VPD |
| * force collection). If not found in the cache, return true. |
| * -- Else return false. |
| * |
| * @param js - JSON Object. |
| * @param filePath - The EEPROM file. |
| * @return true if collection should be attempted, false otherwise. |
| */ |
| static auto needsRecollection(const nlohmann::json& js, const string& filePath) |
| { |
| if (js["frus"][filePath].at(0).value("pluggableAtStandby", false) || |
| js["frus"][filePath].at(0).value("concurrentlyMaintainable", false)) |
| { |
| return true; |
| } |
| if (getBMCState() == "xyz.openbmc_project.State.BMC.BMCState.NotReady") |
| { |
| return true; |
| } |
| if (!isFruInVpdCache(js["frus"][filePath].at(0).value("inventoryPath", ""))) |
| { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * @brief Expands location codes |
| */ |
| static auto expandLocationCode(const string& unexpanded, const Parsed& vpdMap, |
| bool isSystemVpd) |
| { |
| auto expanded{unexpanded}; |
| static constexpr auto SYSTEM_OBJECT = "/system/chassis/motherboard"; |
| static constexpr auto VCEN_IF = "com.ibm.ipzvpd.VCEN"; |
| static constexpr auto VSYS_IF = "com.ibm.ipzvpd.VSYS"; |
| size_t idx = expanded.find("fcs"); |
| try |
| { |
| if (idx != string::npos) |
| { |
| string fc{}; |
| string se{}; |
| if (isSystemVpd) |
| { |
| const auto& fcData = vpdMap.at("VCEN").at("FC"); |
| const auto& seData = vpdMap.at("VCEN").at("SE"); |
| fc = string(fcData.data(), fcData.size()); |
| se = string(seData.data(), seData.size()); |
| } |
| else |
| { |
| fc = readBusProperty(SYSTEM_OBJECT, VCEN_IF, "FC"); |
| se = readBusProperty(SYSTEM_OBJECT, VCEN_IF, "SE"); |
| } |
| |
| // TODO: See if ND0 can be placed in the JSON |
| expanded.replace(idx, 3, fc.substr(0, 4) + ".ND0." + se); |
| } |
| else |
| { |
| idx = expanded.find("mts"); |
| if (idx != string::npos) |
| { |
| string mt{}; |
| string se{}; |
| if (isSystemVpd) |
| { |
| const auto& mtData = vpdMap.at("VSYS").at("TM"); |
| const auto& seData = vpdMap.at("VSYS").at("SE"); |
| mt = string(mtData.data(), mtData.size()); |
| se = string(seData.data(), seData.size()); |
| } |
| else |
| { |
| mt = readBusProperty(SYSTEM_OBJECT, VSYS_IF, "TM"); |
| se = readBusProperty(SYSTEM_OBJECT, VSYS_IF, "SE"); |
| } |
| |
| replace(mt.begin(), mt.end(), '-', '.'); |
| expanded.replace(idx, 3, mt + "." + se); |
| } |
| } |
| } |
| catch (const exception& e) |
| { |
| cerr << "Failed to expand location code with exception: " << e.what() |
| << "\n"; |
| } |
| return expanded; |
| } |
| |
| /** |
| * @brief Populate FRU specific interfaces. |
| * |
| * This is a common method which handles both |
| * ipz and keyword specific interfaces thus, |
| * reducing the code redundancy. |
| * @param[in] map - Reference to the innermost keyword-value map. |
| * @param[in] preIntrStr - Reference to the interface string. |
| * @param[out] interfaces - Reference to interface map. |
| */ |
| template <typename T> |
| static void populateFruSpecificInterfaces(const T& map, |
| const string& preIntrStr, |
| inventory::InterfaceMap& interfaces) |
| { |
| inventory::PropertyMap prop; |
| |
| for (const auto& kwVal : map) |
| { |
| auto kw = kwVal.first; |
| |
| if (kw[0] == '#') |
| { |
| kw = string("PD_") + kw[1]; |
| } |
| else if (isdigit(kw[0])) |
| { |
| kw = string("N_") + kw; |
| } |
| if constexpr (is_same<T, KeywordVpdMap>::value) |
| { |
| if (get_if<Binary>(&kwVal.second)) |
| { |
| Binary vec(get_if<Binary>(&kwVal.second)->begin(), |
| get_if<Binary>(&kwVal.second)->end()); |
| prop.emplace(move(kw), move(vec)); |
| } |
| else |
| { |
| if (kw == "MemorySizeInKB") |
| { |
| inventory::PropertyMap memProp; |
| auto memVal = get_if<size_t>(&kwVal.second); |
| if (memVal) |
| { |
| memProp.emplace(move(kw), |
| ((*memVal) * CONVERT_MB_TO_KB)); |
| interfaces.emplace( |
| "xyz.openbmc_project.Inventory.Item.Dimm", |
| move(memProp)); |
| } |
| else |
| { |
| cerr << "MemorySizeInKB value not found in vpd map\n"; |
| } |
| } |
| } |
| } |
| else |
| { |
| Binary vec(kwVal.second.begin(), kwVal.second.end()); |
| prop.emplace(move(kw), move(vec)); |
| } |
| } |
| |
| interfaces.emplace(preIntrStr, move(prop)); |
| } |
| |
| /** |
| * @brief Populate Interfaces. |
| * |
| * This method populates common and extra interfaces to dbus. |
| * @param[in] js - json object |
| * @param[out] interfaces - Reference to interface map |
| * @param[in] vpdMap - Reference to the parsed vpd map. |
| * @param[in] isSystemVpd - Denotes whether we are collecting the system VPD. |
| */ |
| template <typename T> |
| static void populateInterfaces(const nlohmann::json& js, |
| inventory::InterfaceMap& interfaces, |
| const T& vpdMap, bool isSystemVpd) |
| { |
| for (const auto& ifs : js.items()) |
| { |
| string inf = ifs.key(); |
| inventory::PropertyMap props; |
| |
| for (const auto& itr : ifs.value().items()) |
| { |
| const string& busProp = itr.key(); |
| |
| if (itr.value().is_boolean()) |
| { |
| props.emplace(busProp, itr.value().get<bool>()); |
| } |
| else if (itr.value().is_string()) |
| { |
| if (busProp == "LocationCode" && inf == IBM_LOCATION_CODE_INF) |
| { |
| std::string prop; |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| // TODO deprecate the com.ibm interface later |
| prop = expandLocationCode(itr.value().get<string>(), |
| vpdMap, isSystemVpd); |
| } |
| else if constexpr (is_same<T, KeywordVpdMap>::value) |
| { |
| // Send empty Parsed object to expandLocationCode api. |
| prop = expandLocationCode(itr.value().get<string>(), |
| Parsed{}, false); |
| } |
| props.emplace(busProp, prop); |
| interfaces.emplace(XYZ_LOCATION_CODE_INF, props); |
| interfaces.emplace(IBM_LOCATION_CODE_INF, props); |
| } |
| else |
| { |
| props.emplace(busProp, itr.value().get<string>()); |
| } |
| } |
| else if (itr.value().is_array()) |
| { |
| try |
| { |
| props.emplace(busProp, itr.value().get<Binary>()); |
| } |
| catch (const nlohmann::detail::type_error& e) |
| { |
| std::cerr << "Type exception: " << e.what() << "\n"; |
| // Ignore any type errors |
| } |
| } |
| else if (itr.value().is_object()) |
| { |
| const string& rec = itr.value().value("recordName", ""); |
| const string& kw = itr.value().value("keywordName", ""); |
| const string& encoding = itr.value().value("encoding", ""); |
| |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| if (!rec.empty() && !kw.empty() && vpdMap.count(rec) && |
| vpdMap.at(rec).count(kw)) |
| { |
| auto encoded = |
| encodeKeyword(vpdMap.at(rec).at(kw), encoding); |
| props.emplace(busProp, encoded); |
| } |
| } |
| else if constexpr (is_same<T, KeywordVpdMap>::value) |
| { |
| if (!kw.empty() && vpdMap.count(kw)) |
| { |
| auto kwValue = get_if<Binary>(&vpdMap.at(kw)); |
| auto uintValue = get_if<size_t>(&vpdMap.at(kw)); |
| |
| if (kwValue) |
| { |
| auto prop = |
| string((*kwValue).begin(), (*kwValue).end()); |
| |
| auto encoded = encodeKeyword(prop, encoding); |
| |
| props.emplace(busProp, encoded); |
| } |
| else if (uintValue) |
| { |
| props.emplace(busProp, *uintValue); |
| } |
| } |
| } |
| } |
| else if (itr.value().is_number()) |
| { |
| // For now assume the value is a size_t. In the future it would |
| // be nice to come up with a way to get the type from the JSON. |
| props.emplace(busProp, itr.value().get<size_t>()); |
| } |
| } |
| insertOrMerge(interfaces, inf, move(props)); |
| } |
| } |
| |
| /** |
| * @brief This API checks if this FRU is pcie_devices. If yes then it further |
| * checks whether it is PASS1 planar. |
| */ |
| static bool isThisPcieOnPass1planar(const nlohmann::json& js, |
| const string& file) |
| { |
| auto isThisPCIeDev = false; |
| auto isPASS1 = false; |
| |
| // Check if it is a PCIE device |
| if (js["frus"].find(file) != js["frus"].end()) |
| { |
| if ((js["frus"][file].at(0).find("extraInterfaces") != |
| js["frus"][file].at(0).end())) |
| { |
| if (js["frus"][file].at(0)["extraInterfaces"].find( |
| "xyz.openbmc_project.Inventory.Item.PCIeDevice") != |
| js["frus"][file].at(0)["extraInterfaces"].end()) |
| { |
| isThisPCIeDev = true; |
| } |
| } |
| } |
| |
| if (isThisPCIeDev) |
| { |
| // Collect HW version and SystemType to know if it is PASS1 planar. |
| auto bus = sdbusplus::bus::new_default(); |
| auto property1 = bus.new_method_call( |
| INVENTORY_MANAGER_SERVICE, |
| "/xyz/openbmc_project/inventory/system/chassis/motherboard", |
| "org.freedesktop.DBus.Properties", "Get"); |
| property1.append("com.ibm.ipzvpd.VINI"); |
| property1.append("HW"); |
| auto result1 = bus.call(property1); |
| inventory::Value hwVal; |
| result1.read(hwVal); |
| |
| // SystemType |
| auto property2 = bus.new_method_call( |
| INVENTORY_MANAGER_SERVICE, |
| "/xyz/openbmc_project/inventory/system/chassis/motherboard", |
| "org.freedesktop.DBus.Properties", "Get"); |
| property2.append("com.ibm.ipzvpd.VSBP"); |
| property2.append("IM"); |
| auto result2 = bus.call(property2); |
| inventory::Value imVal; |
| result2.read(imVal); |
| |
| auto pVal1 = get_if<Binary>(&hwVal); |
| auto pVal2 = get_if<Binary>(&imVal); |
| |
| if (pVal1 && pVal2) |
| { |
| auto hwVersion = *pVal1; |
| auto systemType = *pVal2; |
| |
| // IM kw for Everest |
| Binary everestSystem{80, 00, 48, 00}; |
| |
| if (systemType == everestSystem) |
| { |
| if (hwVersion[1] < 21) |
| { |
| isPASS1 = true; |
| } |
| } |
| else if (hwVersion[1] < 2) |
| { |
| isPASS1 = true; |
| } |
| } |
| } |
| |
| return (isThisPCIeDev && isPASS1); |
| } |
| |
| /** Performs any pre-action needed to get the FRU setup for collection. |
| * |
| * @param[in] json - json object |
| * @param[in] file - eeprom file path |
| */ |
| static void preAction(const nlohmann::json& json, const string& file) |
| { |
| if ((json["frus"][file].at(0)).find("preAction") == |
| json["frus"][file].at(0).end()) |
| { |
| return; |
| } |
| |
| if (executePreAction(json, file)) |
| { |
| if (json["frus"][file].at(0).find("devAddress") != |
| json["frus"][file].at(0).end()) |
| { |
| // Now bind the device |
| string bind = json["frus"][file].at(0).value("devAddress", ""); |
| cout << "Binding device " << bind << endl; |
| string bindCmd = string("echo \"") + bind + |
| string("\" > /sys/bus/i2c/drivers/at24/bind"); |
| cout << bindCmd << endl; |
| executeCmd(bindCmd); |
| |
| // Check if device showed up (test for file) |
| if (!fs::exists(file)) |
| { |
| cerr << "EEPROM " << file |
| << " does not exist. Take failure action" << endl; |
| // If not, then take failure postAction |
| executePostFailAction(json, file); |
| } |
| } |
| else |
| { |
| // missing required informations |
| cerr |
| << "VPD inventory JSON missing basic informations of preAction " |
| "for this FRU : [" |
| << file << "]. Executing executePostFailAction." << endl; |
| |
| // Take failure postAction |
| executePostFailAction(json, file); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * @brief Fills the Decorator.AssetTag property into the interfaces map |
| * |
| * This function should only be called in cases where we did not find a JSON |
| * symlink. A missing symlink in /var/lib will be considered as a factory reset |
| * and this function will be used to default the AssetTag property. |
| * |
| * @param interfaces A possibly pre-populated map of inetrfaces to properties. |
| * @param vpdMap A VPD map of the system VPD data. |
| */ |
| static void fillAssetTag(inventory::InterfaceMap& interfaces, |
| const Parsed& vpdMap) |
| { |
| // Read the system serial number and MTM |
| // Default asset tag is Server-MTM-System Serial |
| inventory::Interface assetIntf{ |
| "xyz.openbmc_project.Inventory.Decorator.AssetTag"}; |
| inventory::PropertyMap assetTagProps; |
| std::string defaultAssetTag = |
| std::string{"Server-"} + getKwVal(vpdMap, "VSYS", "TM") + |
| std::string{"-"} + getKwVal(vpdMap, "VSYS", "SE"); |
| assetTagProps.emplace("AssetTag", defaultAssetTag); |
| insertOrMerge(interfaces, assetIntf, std::move(assetTagProps)); |
| } |
| |
| /** |
| * @brief Set certain one time properties in the inventory |
| * Use this function to insert the Functional and Enabled properties into the |
| * inventory map. This function first checks if the object in question already |
| * has these properties hosted on D-Bus, if the property is already there, it is |
| * not modified, hence the name "one time". If the property is not already |
| * present, it will be added to the map with a suitable default value (true for |
| * Functional and false for Enabled) |
| * |
| * @param[in] object - The inventory D-Bus obejct without the inventory prefix. |
| * @param[inout] interfaces - Reference to a map of inventory interfaces to |
| * which the properties will be attached. |
| */ |
| static void setOneTimeProperties(const std::string& object, |
| inventory::InterfaceMap& interfaces) |
| { |
| auto bus = sdbusplus::bus::new_default(); |
| auto objectPath = INVENTORY_PATH + object; |
| auto prop = bus.new_method_call("xyz.openbmc_project.Inventory.Manager", |
| objectPath.c_str(), |
| "org.freedesktop.DBus.Properties", "Get"); |
| prop.append("xyz.openbmc_project.State.Decorator.OperationalStatus"); |
| prop.append("Functional"); |
| try |
| { |
| auto result = bus.call(prop); |
| } |
| catch (const sdbusplus::exception::SdBusError& e) |
| { |
| // Treat as property unavailable |
| inventory::PropertyMap prop; |
| prop.emplace("Functional", true); |
| interfaces.emplace( |
| "xyz.openbmc_project.State.Decorator.OperationalStatus", |
| move(prop)); |
| } |
| prop = bus.new_method_call("xyz.openbmc_project.Inventory.Manager", |
| objectPath.c_str(), |
| "org.freedesktop.DBus.Properties", "Get"); |
| prop.append("xyz.openbmc_project.Object.Enable"); |
| prop.append("Enabled"); |
| try |
| { |
| auto result = bus.call(prop); |
| } |
| catch (const sdbusplus::exception::SdBusError& e) |
| { |
| // Treat as property unavailable |
| inventory::PropertyMap prop; |
| prop.emplace("Enabled", false); |
| interfaces.emplace("xyz.openbmc_project.Object.Enable", move(prop)); |
| } |
| } |
| |
| /** |
| * @brief Prime the Inventory |
| * Prime the inventory by populating only the location code, |
| * type interface and the inventory object for the frus |
| * which are not system vpd fru. |
| * |
| * @param[in] jsObject - Reference to vpd inventory json object |
| * @param[in] vpdMap - Reference to the parsed vpd map |
| * |
| * @returns Map of items in extraInterface. |
| */ |
| template <typename T> |
| inventory::ObjectMap primeInventory(const nlohmann::json& jsObject, |
| const T& vpdMap) |
| { |
| inventory::ObjectMap objects; |
| |
| for (auto& itemFRUS : jsObject["frus"].items()) |
| { |
| for (auto& itemEEPROM : itemFRUS.value()) |
| { |
| // Take pre actions if needed |
| if (itemEEPROM.find("preAction") != itemEEPROM.end()) |
| { |
| preAction(jsObject, itemFRUS.key()); |
| } |
| |
| inventory::InterfaceMap interfaces; |
| inventory::Object object(itemEEPROM.at("inventoryPath")); |
| |
| if ((itemFRUS.key() != systemVpdFilePath) && |
| !itemEEPROM.value("noprime", false)) |
| { |
| inventory::PropertyMap presProp; |
| |
| // Do not populate Present property for frus whose |
| // synthesized=true. synthesized=true says the fru is owned by |
| // some other component and not by vpd. |
| if (!itemEEPROM.value("synthesized", false)) |
| { |
| presProp.emplace("Present", false); |
| interfaces.emplace("xyz.openbmc_project.Inventory.Item", |
| presProp); |
| } |
| setOneTimeProperties(object, interfaces); |
| if (itemEEPROM.find("extraInterfaces") != itemEEPROM.end()) |
| { |
| for (const auto& eI : itemEEPROM["extraInterfaces"].items()) |
| { |
| inventory::PropertyMap props; |
| if (eI.key() == IBM_LOCATION_CODE_INF) |
| { |
| if constexpr (std::is_same<T, Parsed>::value) |
| { |
| for (auto& lC : eI.value().items()) |
| { |
| auto propVal = expandLocationCode( |
| lC.value().get<string>(), vpdMap, true); |
| |
| props.emplace(move(lC.key()), |
| move(propVal)); |
| interfaces.emplace(XYZ_LOCATION_CODE_INF, |
| props); |
| interfaces.emplace(move(eI.key()), |
| move(props)); |
| } |
| } |
| } |
| else if (eI.key().find("Inventory.Item.") != |
| string::npos) |
| { |
| interfaces.emplace(move(eI.key()), move(props)); |
| } |
| else if (eI.key() == |
| "xyz.openbmc_project.Inventory.Item") |
| { |
| for (auto& val : eI.value().items()) |
| { |
| if (val.key() == "PrettyName") |
| { |
| presProp.emplace(val.key(), |
| val.value().get<string>()); |
| } |
| } |
| // Use insert_or_assign here as we may already have |
| // inserted the present property only earlier in |
| // this function under this same interface. |
| interfaces.insert_or_assign(eI.key(), |
| move(presProp)); |
| } |
| } |
| } |
| objects.emplace(move(object), move(interfaces)); |
| } |
| } |
| } |
| return objects; |
| } |
| |
| /** |
| * @brief This API executes command to set environment variable |
| * And then reboot the system |
| * @param[in] key -env key to set new value |
| * @param[in] value -value to set. |
| */ |
| void setEnvAndReboot(const string& key, const string& value) |
| { |
| // set env and reboot and break. |
| executeCmd("/sbin/fw_setenv", key, value); |
| log<level::INFO>("Rebooting BMC to pick up new device tree"); |
| // make dbus call to reboot |
| auto bus = sdbusplus::bus::new_default_system(); |
| auto method = bus.new_method_call( |
| "org.freedesktop.systemd1", "/org/freedesktop/systemd1", |
| "org.freedesktop.systemd1.Manager", "Reboot"); |
| bus.call_noreply(method); |
| } |
| |
| /* |
| * @brief This API checks for env var fitconfig. |
| * If not initialised OR updated as per the current system type, |
| * update this env var and reboot the system. |
| * |
| * @param[in] systemType IM kwd in vpd tells about which system type it is. |
| * */ |
| void setDevTreeEnv(const string& systemType) |
| { |
| // Init with default dtb |
| string newDeviceTree = "conf-aspeed-bmc-ibm-rainier-p1.dtb"; |
| static const deviceTreeMap deviceTreeSystemTypeMap = { |
| {RAINIER_2U, "conf-aspeed-bmc-ibm-rainier-p1.dtb"}, |
| {RAINIER_2U_V2, "conf-aspeed-bmc-ibm-rainier.dtb"}, |
| {RAINIER_4U, "conf-aspeed-bmc-ibm-rainier-4u-p1.dtb"}, |
| {RAINIER_4U_V2, "conf-aspeed-bmc-ibm-rainier-4u.dtb"}, |
| {RAINIER_1S4U, "conf-aspeed-bmc-ibm-rainier-1s4u.dtb"}, |
| {EVEREST, "conf-aspeed-bmc-ibm-everest.dtb"}, |
| {EVEREST_V2, "conf-aspeed-bmc-ibm-everest.dtb"}}; |
| |
| if (deviceTreeSystemTypeMap.find(systemType) != |
| deviceTreeSystemTypeMap.end()) |
| { |
| newDeviceTree = deviceTreeSystemTypeMap.at(systemType); |
| } |
| else |
| { |
| // System type not supported |
| string err = "This System type not found/supported in dtb table " + |
| systemType + |
| ".Please check the HW and IM keywords in the system " |
| "VPD.Breaking..."; |
| |
| // map to hold additional data in case of logging pel |
| PelAdditionalData additionalData{}; |
| additionalData.emplace("DESCRIPTION", err); |
| createPEL(additionalData, PelSeverity::WARNING, |
| errIntfForInvalidSystemType); |
| exit(-1); |
| } |
| |
| string readVarValue; |
| bool envVarFound = false; |
| |
| vector<string> output = executeCmd("/sbin/fw_printenv"); |
| for (const auto& entry : output) |
| { |
| size_t pos = entry.find("="); |
| string key = entry.substr(0, pos); |
| if (key != "fitconfig") |
| { |
| continue; |
| } |
| |
| envVarFound = true; |
| if (pos + 1 < entry.size()) |
| { |
| readVarValue = entry.substr(pos + 1); |
| if (readVarValue.find(newDeviceTree) != string::npos) |
| { |
| // fitconfig is Updated. No action needed |
| break; |
| } |
| } |
| // set env and reboot and break. |
| setEnvAndReboot(key, newDeviceTree); |
| exit(0); |
| } |
| |
| // check If env var Not found |
| if (!envVarFound) |
| { |
| setEnvAndReboot("fitconfig", newDeviceTree); |
| } |
| } |
| |
| /** |
| * @brief API to check if we need to restore system VPD |
| * This functionality is only applicable for IPZ VPD data. |
| * @param[in] vpdMap - IPZ vpd map |
| * @param[in] objectPath - Object path for the FRU |
| */ |
| void restoreSystemVPD(Parsed& vpdMap, const string& objectPath) |
| { |
| for (const auto& systemRecKwdPair : svpdKwdMap) |
| { |
| auto it = vpdMap.find(systemRecKwdPair.first); |
| |
| // check if record is found in map we got by parser |
| if (it != vpdMap.end()) |
| { |
| const auto& kwdListForRecord = systemRecKwdPair.second; |
| for (const auto& keyword : kwdListForRecord) |
| { |
| DbusPropertyMap& kwdValMap = it->second; |
| auto iterator = kwdValMap.find(keyword); |
| |
| if (iterator != kwdValMap.end()) |
| { |
| string& kwdValue = iterator->second; |
| |
| // check bus data |
| const string& recordName = systemRecKwdPair.first; |
| const string& busValue = readBusProperty( |
| objectPath, ipzVpdInf + recordName, keyword); |
| |
| std::string defaultValue{' '}; |
| |
| // Explicit check for D0 is required as this keyword will |
| // never be blank and 0x00 should be treated as no value in |
| // this case. |
| if (recordName == "UTIL" && keyword == "D0") |
| { |
| // default value of kwd D0 is 0x00. This kwd will never |
| // be blank. |
| defaultValue = '\0'; |
| } |
| |
| if (busValue.find_first_not_of(defaultValue) != |
| string::npos) |
| { |
| if (kwdValue.find_first_not_of(defaultValue) != |
| string::npos) |
| { |
| // both the data are present, check for mismatch |
| if (busValue != kwdValue) |
| { |
| string errMsg = "VPD data mismatch on cache " |
| "and hardware for record: "; |
| errMsg += (*it).first; |
| errMsg += " and keyword: "; |
| errMsg += keyword; |
| |
| // data mismatch |
| PelAdditionalData additionalData; |
| additionalData.emplace("CALLOUT_INVENTORY_PATH", |
| objectPath); |
| |
| additionalData.emplace("DESCRIPTION", errMsg); |
| |
| createPEL(additionalData, PelSeverity::WARNING, |
| errIntfForInvalidVPD); |
| } |
| } |
| else |
| { |
| // implies hardware data is blank |
| // update the map |
| Binary busData(busValue.begin(), busValue.end()); |
| |
| // update the map as well, so that cache data is not |
| // updated as blank while populating VPD map on Dbus |
| // in populateDBus Api |
| kwdValue = busValue; |
| } |
| } |
| else if (kwdValue.find_first_not_of(defaultValue) == |
| string::npos) |
| { |
| string errMsg = "VPD is blank on both cache and " |
| "hardware for record: "; |
| errMsg += (*it).first; |
| errMsg += " and keyword: "; |
| errMsg += keyword; |
| errMsg += ". SSR need to update hardware VPD."; |
| |
| // both the data are blanks, log PEL |
| PelAdditionalData additionalData; |
| additionalData.emplace("CALLOUT_INVENTORY_PATH", |
| objectPath); |
| |
| additionalData.emplace("DESCRIPTION", errMsg); |
| |
| // log PEL TODO: Block IPL |
| createPEL(additionalData, PelSeverity::ERROR, |
| errIntfForBlankSystemVPD); |
| continue; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @brief This checks for is this FRU a processor |
| * And if yes, then checks for is this primary |
| * |
| * @param[in] js- vpd json to get the information about this FRU |
| * @param[in] filePath- FRU vpd |
| * |
| * @return true/false |
| */ |
| bool isThisPrimaryProcessor(nlohmann::json& js, const string& filePath) |
| { |
| bool isProcessor = false; |
| bool isPrimary = false; |
| |
| for (const auto& item : js["frus"][filePath]) |
| { |
| if (item.find("extraInterfaces") != item.end()) |
| { |
| for (const auto& eI : item["extraInterfaces"].items()) |
| { |
| if (eI.key().find("Inventory.Item.Cpu") != string::npos) |
| { |
| isProcessor = true; |
| } |
| } |
| } |
| |
| if (isProcessor) |
| { |
| string cpuType = item.value("cpuType", ""); |
| if (cpuType == "primary") |
| { |
| isPrimary = true; |
| } |
| } |
| } |
| |
| return (isProcessor && isPrimary); |
| } |
| |
| /** |
| * @brief This finds DIMM vpd in vpd json and enables them by binding the device |
| * driver |
| * @param[in] js- vpd json to iterate through and take action if it is DIMM |
| */ |
| void doEnableAllDimms(nlohmann::json& js) |
| { |
| // iterate over each fru |
| for (const auto& eachFru : js["frus"].items()) |
| { |
| // skip the driver binding if eeprom already exists |
| if (fs::exists(eachFru.key())) |
| { |
| continue; |
| } |
| |
| for (const auto& eachInventory : eachFru.value()) |
| { |
| if (eachInventory.find("extraInterfaces") != eachInventory.end()) |
| { |
| for (const auto& eI : eachInventory["extraInterfaces"].items()) |
| { |
| if (eI.key().find("Inventory.Item.Dimm") != string::npos) |
| { |
| string dimmVpd = eachFru.key(); |
| // fetch it from |
| // "/sys/bus/i2c/drivers/at24/414-0050/eeprom" |
| |
| regex matchPatern("([0-9]+-[0-9]{4})"); |
| smatch matchFound; |
| if (regex_search(dimmVpd, matchFound, matchPatern)) |
| { |
| vector<string> i2cReg; |
| boost::split(i2cReg, matchFound.str(0), |
| boost::is_any_of("-")); |
| |
| // remove 0s from begining |
| const regex pattern("^0+(?!$)"); |
| for (auto& i : i2cReg) |
| { |
| i = regex_replace(i, pattern, ""); |
| } |
| |
| if (i2cReg.size() == 2) |
| { |
| // echo 24c32 0x50 > |
| // /sys/bus/i2c/devices/i2c-16/new_device |
| string cmnd = "echo 24c32 0x" + i2cReg[1] + |
| " > /sys/bus/i2c/devices/i2c-" + |
| i2cReg[0] + "/new_device"; |
| |
| executeCmd(cmnd); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @brief Check if the given CPU is an IO only chip. |
| * The CPU is termed as IO, whose all of the cores are bad and can never be |
| * used. Those CPU chips can be used for IO purpose like connecting PCIe devices |
| * etc., The CPU whose every cores are bad, can be identified from the CP00 |
| * record's PG keyword, only if all of the 8 EQs' value equals 0xE7F9FF. (1EQ |
| * has 4 cores grouped together by sharing its cache memory.) |
| * @param [in] pgKeyword - PG Keyword of CPU. |
| * @return true if the given cpu is an IO, false otherwise. |
| */ |
| static bool isCPUIOGoodOnly(const string& pgKeyword) |
| { |
| const unsigned char io[] = {0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, |
| 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, |
| 0xF9, 0xFF, 0xE7, 0xF9, 0xFF, 0xE7, 0xF9, 0xFF}; |
| // EQ0 index (in PG keyword) starts at 97 (with offset starting from 0). |
| // Each EQ carries 3 bytes of data. Totally there are 8 EQs. If all EQs' |
| // value equals 0xE7F9FF, then the cpu has no good cores and its treated as |
| // IO. |
| if (memcmp(io, pgKeyword.data() + 97, 24) == 0) |
| { |
| return true; |
| } |
| |
| // The CPU is not an IO |
| return false; |
| } |
| |
| /** |
| * @brief Populate Dbus. |
| * This method invokes all the populateInterface functions |
| * and notifies PIM about dbus object. |
| * @param[in] vpdMap - Either IPZ vpd map or Keyword vpd map based on the |
| * input. |
| * @param[in] js - Inventory json object |
| * @param[in] filePath - Path of the vpd file |
| * @param[in] preIntrStr - Interface string |
| */ |
| template <typename T> |
| static void populateDbus(T& vpdMap, nlohmann::json& js, const string& filePath) |
| { |
| inventory::InterfaceMap interfaces; |
| inventory::ObjectMap objects; |
| inventory::PropertyMap prop; |
| string ccinFromVpd; |
| |
| bool isSystemVpd = (filePath == systemVpdFilePath); |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| ccinFromVpd = getKwVal(vpdMap, "VINI", "CC"); |
| transform(ccinFromVpd.begin(), ccinFromVpd.end(), ccinFromVpd.begin(), |
| ::toupper); |
| |
| if (isSystemVpd) |
| { |
| std::vector<std::string> interfaces = {motherBoardInterface}; |
| // call mapper to check for object path creation |
| MapperResponse subTree = |
| getObjectSubtreeForInterfaces(pimPath, 0, interfaces); |
| string mboardPath = |
| js["frus"][filePath].at(0).value("inventoryPath", ""); |
| |
| // Attempt system VPD restore if we have a motherboard |
| // object in the inventory. |
| if ((subTree.size() != 0) && |
| (subTree.find(pimPath + mboardPath) != subTree.end())) |
| { |
| restoreSystemVPD(vpdMap, mboardPath); |
| } |
| else |
| { |
| log<level::ERR>("No object path found"); |
| } |
| } |
| else |
| { |
| // check if it is processor vpd. |
| auto isPrimaryCpu = isThisPrimaryProcessor(js, filePath); |
| |
| if (isPrimaryCpu) |
| { |
| auto ddVersion = getKwVal(vpdMap, "CRP0", "DD"); |
| |
| auto chipVersion = atoi(ddVersion.substr(1, 2).c_str()); |
| |
| if (chipVersion >= 2) |
| { |
| doEnableAllDimms(js); |
| } |
| } |
| } |
| } |
| |
| auto processFactoryReset = false; |
| |
| if (isSystemVpd) |
| { |
| string systemJsonName{}; |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| // pick the right system json |
| systemJsonName = getSystemsJson(vpdMap); |
| } |
| |
| fs::path target = systemJsonName; |
| fs::path link = INVENTORY_JSON_SYM_LINK; |
| |
| // If the symlink does not exist, we treat that as a factory reset |
| processFactoryReset = !fs::exists(INVENTORY_JSON_SYM_LINK); |
| |
| // Create the directory for hosting the symlink |
| fs::create_directories(VPD_FILES_PATH); |
| // unlink the symlink previously created (if any) |
| remove(INVENTORY_JSON_SYM_LINK); |
| // create a new symlink based on the system |
| fs::create_symlink(target, link); |
| |
| // Reloading the json |
| ifstream inventoryJson(link); |
| js = json::parse(inventoryJson); |
| inventoryJson.close(); |
| } |
| |
| for (const auto& item : js["frus"][filePath]) |
| { |
| const auto& objectPath = item["inventoryPath"]; |
| sdbusplus::message::object_path object(objectPath); |
| |
| vector<string> ccinList; |
| if (item.find("ccin") != item.end()) |
| { |
| for (const auto& cc : item["ccin"]) |
| { |
| string ccin = cc; |
| transform(ccin.begin(), ccin.end(), ccin.begin(), ::toupper); |
| ccinList.push_back(ccin); |
| } |
| } |
| |
| if (!ccinFromVpd.empty() && !ccinList.empty() && |
| (find(ccinList.begin(), ccinList.end(), ccinFromVpd) == |
| ccinList.end())) |
| { |
| continue; |
| } |
| |
| if ((isSystemVpd) || (item.value("noprime", false))) |
| { |
| |
| // Populate one time properties for the system VPD and its sub-frus |
| // and for other non-primeable frus. |
| // For the remaining FRUs, this will get handled as a part of |
| // priming the inventory. |
| setOneTimeProperties(objectPath, interfaces); |
| } |
| |
| // Populate the VPD keywords and the common interfaces only if we |
| // are asked to inherit that data from the VPD, else only add the |
| // extraInterfaces. |
| if (item.value("inherit", true)) |
| { |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| // Each record in the VPD becomes an interface and all |
| // keyword within the record are properties under that |
| // interface. |
| for (const auto& record : vpdMap) |
| { |
| populateFruSpecificInterfaces( |
| record.second, ipzVpdInf + record.first, interfaces); |
| } |
| } |
| else if constexpr (is_same<T, KeywordVpdMap>::value) |
| { |
| populateFruSpecificInterfaces(vpdMap, kwdVpdInf, interfaces); |
| } |
| if (js.find("commonInterfaces") != js.end()) |
| { |
| populateInterfaces(js["commonInterfaces"], interfaces, vpdMap, |
| isSystemVpd); |
| } |
| } |
| else |
| { |
| // Check if we have been asked to inherit specific record(s) |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| if (item.find("copyRecords") != item.end()) |
| { |
| for (const auto& record : item["copyRecords"]) |
| { |
| const string& recordName = record; |
| if (vpdMap.find(recordName) != vpdMap.end()) |
| { |
| populateFruSpecificInterfaces( |
| vpdMap.at(recordName), ipzVpdInf + recordName, |
| interfaces); |
| } |
| } |
| } |
| } |
| } |
| // Populate interfaces and properties that are common to every FRU |
| // and additional interface that might be defined on a per-FRU |
| // basis. |
| if (item.find("extraInterfaces") != item.end()) |
| { |
| populateInterfaces(item["extraInterfaces"], interfaces, vpdMap, |
| isSystemVpd); |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| if (item["extraInterfaces"].find( |
| "xyz.openbmc_project.Inventory.Item.Cpu") != |
| item["extraInterfaces"].end()) |
| { |
| if (isCPUIOGoodOnly(getKwVal(vpdMap, "CP00", "PG"))) |
| { |
| interfaces[invItemIntf]["PrettyName"] = "IO Module"; |
| } |
| } |
| } |
| } |
| |
| // embedded property(true or false) says whether the subfru is embedded |
| // into the parent fru (or) not. VPD sets Present property only for |
| // embedded frus. If the subfru is not an embedded FRU, the subfru may |
| // or may not be physically present. Those non embedded frus will always |
| // have Present=false irrespective of its physical presence or absence. |
| // Eg: nvme drive in nvme slot is not an embedded FRU. So don't set |
| // Present to true for such sub frus. |
| // Eg: ethernet port is embedded into bmc card. So set Present to true |
| // for such sub frus. Also donot populate present property for embedded |
| // subfru which is synthesized. Currently there is no subfru which are |
| // both embedded and synthesized. But still the case is handled here. |
| if ((item.value("embedded", true)) && |
| (!item.value("synthesized", false))) |
| { |
| inventory::PropertyMap presProp; |
| presProp.emplace("Present", true); |
| insertOrMerge(interfaces, invItemIntf, move(presProp)); |
| } |
| |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| // Restore asset tag, if needed |
| if (processFactoryReset && objectPath == "/system") |
| { |
| fillAssetTag(interfaces, vpdMap); |
| } |
| } |
| |
| objects.emplace(move(object), move(interfaces)); |
| } |
| |
| if (isSystemVpd) |
| { |
| inventory::ObjectMap primeObject = primeInventory(js, vpdMap); |
| objects.insert(primeObject.begin(), primeObject.end()); |
| |
| // set the U-boot environment variable for device-tree |
| if constexpr (is_same<T, Parsed>::value) |
| { |
| setDevTreeEnv(fs::path(getSystemsJson(vpdMap)).filename()); |
| } |
| } |
| |
| // Notify PIM |
| common::utility::callPIM(move(objects)); |
| } |
| |
| int main(int argc, char** argv) |
| { |
| int rc = 0; |
| json js{}; |
| Binary vpdVector{}; |
| string file{}; |
| // map to hold additional data in case of logging pel |
| PelAdditionalData additionalData{}; |
| |
| // this is needed to hold base fru inventory path in case there is ECC or |
| // vpd exception while parsing the file |
| std::string baseFruInventoryPath = {}; |
| |
| // severity for PEL |
| PelSeverity pelSeverity = PelSeverity::WARNING; |
| |
| try |
| { |
| App app{"ibm-read-vpd - App to read IPZ format VPD, parse it and store " |
| "in DBUS"}; |
| |
| app.add_option("-f, --file", file, "File containing VPD (IPZ/KEYWORD)") |
| ->required(); |
| |
| CLI11_PARSE(app, argc, argv); |
| |
| // PEL severity should be ERROR in case of any system VPD failure |
| if (file == systemVpdFilePath) |
| { |
| pelSeverity = PelSeverity::ERROR; |
| } |
| |
| auto jsonToParse = INVENTORY_JSON_DEFAULT; |
| |
| // If the symlink exists, it means it has been setup for us, switch the |
| // path |
| if (fs::exists(INVENTORY_JSON_SYM_LINK)) |
| { |
| jsonToParse = INVENTORY_JSON_SYM_LINK; |
| } |
| |
| // Make sure that the file path we get is for a supported EEPROM |
| ifstream inventoryJson(jsonToParse); |
| if (!inventoryJson) |
| { |
| throw(VpdJsonException("Failed to access Json path", jsonToParse)); |
| } |
| |
| try |
| { |
| js = json::parse(inventoryJson); |
| } |
| catch (const json::parse_error& ex) |
| { |
| throw(VpdJsonException("Json parsing failed", jsonToParse)); |
| } |
| |
| // Do we have the mandatory "frus" section? |
| if (js.find("frus") == js.end()) |
| { |
| throw(VpdJsonException("FRUs section not found in JSON", |
| jsonToParse)); |
| } |
| |
| // Check if it's a udev path - patterned as(/ahb/ahb:apb/ahb:apb:bus@) |
| if (file.find("/ahb:apb") != string::npos) |
| { |
| // Translate udev path to a generic /sys/bus/.. file path. |
| udevToGenericPath(file); |
| |
| if ((js["frus"].find(file) != js["frus"].end()) && |
| (file == systemVpdFilePath)) |
| { |
| // We have already collected system VPD, skip. |
| return 0; |
| } |
| } |
| |
| if (file.empty()) |
| { |
| cerr << "The EEPROM path <" << file << "> is not valid."; |
| return 0; |
| } |
| if (js["frus"].find(file) == js["frus"].end()) |
| { |
| return 0; |
| } |
| |
| if (!fs::exists(file)) |
| { |
| cout << "Device path: " << file |
| << " does not exist. Spurious udev event? Exiting." << endl; |
| return 0; |
| } |
| |
| baseFruInventoryPath = js["frus"][file][0]["inventoryPath"]; |
| // Check if we can read the VPD file based on the power state |
| // We skip reading VPD when the power is ON in two scenarios: |
| // 1) The eeprom we are trying to read is that of the system VPD and the |
| // JSON symlink is already setup (the symlink's existence tells us we |
| // are not coming out of a factory reset) |
| // 2) The JSON tells us that the FRU EEPROM cannot be |
| // read when we are powered ON. |
| if (js["frus"][file].at(0).value("powerOffOnly", false) || |
| (file == systemVpdFilePath && fs::exists(INVENTORY_JSON_SYM_LINK))) |
| { |
| if ("xyz.openbmc_project.State.Chassis.PowerState.On" == |
| getPowerState()) |
| { |
| cout << "This VPD cannot be read when power is ON" << endl; |
| return 0; |
| } |
| } |
| |
| // Check if this VPD should be recollected at all |
| if (!needsRecollection(js, file)) |
| { |
| cout << "Skip VPD recollection for: " << file << endl; |
| return 0; |
| } |
| |
| try |
| { |
| vpdVector = getVpdDataInVector(js, file); |
| ParserInterface* parser = ParserFactory::getParser(vpdVector); |
| variant<KeywordVpdMap, Store> parseResult; |
| parseResult = parser->parse(); |
| |
| if (auto pVal = get_if<Store>(&parseResult)) |
| { |
| populateDbus(pVal->getVpdMap(), js, file); |
| } |
| else if (auto pVal = get_if<KeywordVpdMap>(&parseResult)) |
| { |
| populateDbus(*pVal, js, file); |
| } |
| |
| // release the parser object |
| ParserFactory::freeParser(parser); |
| } |
| catch (const exception& e) |
| { |
| executePostFailAction(js, file); |
| throw; |
| } |
| } |
| catch (const VpdJsonException& ex) |
| { |
| additionalData.emplace("JSON_PATH", ex.getJsonPath()); |
| additionalData.emplace("DESCRIPTION", ex.what()); |
| createPEL(additionalData, pelSeverity, errIntfForJsonFailure); |
| |
| cerr << ex.what() << "\n"; |
| rc = -1; |
| } |
| catch (const VpdEccException& ex) |
| { |
| additionalData.emplace("DESCRIPTION", "ECC check failed"); |
| additionalData.emplace("CALLOUT_INVENTORY_PATH", |
| INVENTORY_PATH + baseFruInventoryPath); |
| createPEL(additionalData, pelSeverity, errIntfForEccCheckFail); |
| dumpBadVpd(file, vpdVector); |
| cerr << ex.what() << "\n"; |
| rc = -1; |
| } |
| catch (const VpdDataException& ex) |
| { |
| if (isThisPcieOnPass1planar(js, file)) |
| { |
| cout << "Pcie_device [" << file |
| << "]'s VPD is not valid on PASS1 planar.Ignoring.\n"; |
| rc = 0; |
| } |
| else if (!(isPresent(js, file).value_or(true))) |
| { |
| cout << "FRU at: " << file |
| << " is not detected present. Ignore parser error.\n"; |
| rc = 0; |
| } |
| else |
| { |
| string errorMsg = |
| "VPD file is either empty or invalid. Parser failed for ["; |
| errorMsg += file; |
| errorMsg += "], with error = " + std::string(ex.what()); |
| |
| additionalData.emplace("DESCRIPTION", errorMsg); |
| additionalData.emplace("CALLOUT_INVENTORY_PATH", |
| INVENTORY_PATH + baseFruInventoryPath); |
| createPEL(additionalData, pelSeverity, errIntfForInvalidVPD); |
| |
| rc = -1; |
| } |
| } |
| catch (const exception& e) |
| { |
| dumpBadVpd(file, vpdVector); |
| cerr << e.what() << "\n"; |
| rc = -1; |
| } |
| |
| return rc; |
| } |