1 /**
2  * Copyright © 2019 IBM 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 #include "config.h"
17 
18 #include "version.hpp"
19 
20 #include "pmbus.hpp"
21 #include "utility.hpp"
22 
23 #include <phosphor-logging/log.hpp>
24 #include <xyz/openbmc_project/Common/Device/error.hpp>
25 
26 #include <exception>
27 #include <iostream>
28 #include <regex>
29 #include <stdexcept>
30 #include <tuple>
31 
32 using json = nlohmann::json;
33 
34 using namespace phosphor::logging;
35 
36 using namespace phosphor::power::util;
37 using namespace sdbusplus::xyz::openbmc_project::Common::Device::Error;
38 
39 namespace version
40 {
41 namespace utils
42 {
43 constexpr auto IBMCFFPSInterface =
44     "xyz.openbmc_project.Configuration.IBMCFFPSConnector";
45 constexpr auto i2cBusProp = "I2CBus";
46 constexpr auto i2cAddressProp = "I2CAddress";
47 
getVersionInfo(const std::string & psuInventoryPath)48 PsuVersionInfo getVersionInfo(const std::string& psuInventoryPath)
49 {
50     auto data = phosphor::power::util::loadJSONFromFile(PSU_JSON_PATH);
51 
52     if (data == nullptr)
53     {
54         return {};
55     }
56 
57     auto devices = data.find("psuDevices");
58     if (devices == data.end())
59     {
60         log<level::WARNING>("Unable to find psuDevices");
61         return {};
62     }
63     auto devicePath = devices->find(psuInventoryPath);
64     if (devicePath == devices->end())
65     {
66         log<level::WARNING>("Unable to find path for PSU",
67                             entry("PATH=%s", psuInventoryPath.c_str()));
68         return {};
69     }
70 
71     auto type = phosphor::power::util::getPMBusAccessType(data);
72 
73     std::string versionStr;
74     for (const auto& fru : data["fruConfigs"])
75     {
76         if (fru["propertyName"] == "Version")
77         {
78             versionStr = fru["fileName"].get<std::string>();
79             break;
80         }
81     }
82     if (versionStr.empty())
83     {
84         log<level::WARNING>("Unable to find Version file");
85         return {};
86     }
87     return std::make_tuple(*devicePath, type, versionStr);
88 }
89 
90 // A default implemention compare the string itself
getLatestDefault(const std::vector<std::string> & versions)91 std::string getLatestDefault(const std::vector<std::string>& versions)
92 {
93     std::string latest;
94     for (const auto& version : versions)
95     {
96         if (latest < version)
97         {
98             latest = version;
99         }
100     }
101     return latest;
102 }
103 
getPsuI2c(sdbusplus::bus_t & bus,const std::string & psuInventoryPath)104 PsuI2cInfo getPsuI2c(sdbusplus::bus_t& bus, const std::string& psuInventoryPath)
105 {
106     auto depth = 0;
107     auto objects = getSubTree(bus, "/", IBMCFFPSInterface, depth);
108     if (objects.empty())
109     {
110         throw std::runtime_error("Supported Configuration Not Found");
111     }
112 
113     std::optional<std::uint64_t> i2cbus;
114     std::optional<std::uint64_t> i2caddr;
115 
116     // GET a map of objects back.
117     // Each object will have a path, a service, and an interface.
118     for (const auto& [path, services] : objects)
119     {
120         auto service = services.begin()->first;
121 
122         if (path.empty() || service.empty())
123         {
124             continue;
125         }
126 
127         // Match the PSU identifier in the path with the passed PSU inventory
128         // path. Compare the last character of both paths to find the PSU bus
129         // and address. example: PSU path:
130         // /xyz/openbmc_project/inventory/system/board/Nisqually_Backplane/Power_Supply_Slot_0
131         // PSU inventory path:
132         // /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0
133         if (path.back() == psuInventoryPath.back())
134         {
135             // Retrieve i2cBus and i2cAddress from array of properties.
136             auto properties =
137                 getAllProperties(bus, path, IBMCFFPSInterface, service);
138             for (const auto& property : properties)
139             {
140                 try
141                 {
142                     if (property.first == i2cBusProp)
143                     {
144                         i2cbus = std::get<uint64_t>(properties.at(i2cBusProp));
145                     }
146                     else if (property.first == i2cAddressProp)
147                     {
148                         i2caddr =
149                             std::get<uint64_t>(properties.at(i2cAddressProp));
150                     }
151                 }
152                 catch (const std::exception& e)
153                 {
154                     log<level::WARNING>(
155                         std::format("Error reading property {}: {}",
156                                     property.first, e.what())
157                             .c_str());
158                 }
159             }
160 
161             if (i2cbus.has_value() && i2caddr.has_value())
162             {
163                 break;
164             }
165         }
166     }
167 
168     if (!i2cbus.has_value() || !i2caddr.has_value())
169     {
170         throw std::runtime_error("Failed to get I2C bus or address");
171     }
172 
173     return std::make_tuple(*i2cbus, *i2caddr);
174 }
175 
176 std::unique_ptr<phosphor::pmbus::PMBusBase>
getPmbusIntf(std::uint64_t i2cBus,std::uint64_t i2cAddr)177     getPmbusIntf(std::uint64_t i2cBus, std::uint64_t i2cAddr)
178 {
179     std::stringstream ss;
180     ss << std::hex << std::setw(4) << std::setfill('0') << i2cAddr;
181     return phosphor::pmbus::createPMBus(i2cBus, ss.str());
182 }
183 
readVPDValue(phosphor::pmbus::PMBusBase & pmbusIntf,const std::string & vpdName,const phosphor::pmbus::Type & type,const std::size_t & vpdSize)184 std::string readVPDValue(phosphor::pmbus::PMBusBase& pmbusIntf,
185                          const std::string& vpdName,
186                          const phosphor::pmbus::Type& type,
187                          const std::size_t& vpdSize)
188 {
189     std::string vpdValue;
190     const std::regex illegalVPDRegex =
191         std::regex("[^[:alnum:]]", std::regex::basic);
192 
193     try
194     {
195         vpdValue = pmbusIntf.readString(vpdName, type);
196     }
197     catch (const ReadFailure& e)
198     {
199         // Ignore the read failure, let pmbus code indicate failure.
200     }
201 
202     if (vpdValue.size() != vpdSize)
203     {
204         log<level::INFO>(
205             std::format(" {} resize needed. size: {}", vpdName, vpdValue.size())
206                 .c_str());
207         vpdValue.resize(vpdSize, ' ');
208     }
209 
210     // Replace any illegal values with space(s).
211     std::regex_replace(vpdValue.begin(), vpdValue.begin(), vpdValue.end(),
212                        illegalVPDRegex, " ");
213 
214     return vpdValue;
215 }
216 
checkFileExists(const std::string & filePath)217 bool checkFileExists(const std::string& filePath)
218 {
219     try
220     {
221         return std::filesystem::exists(filePath);
222     }
223     catch (const std::exception& e)
224     {
225         log<level::ERR>(std::format("Unable to check for existence of {}: {}",
226                                     filePath, e.what())
227                             .c_str());
228     }
229     return false;
230 }
231 } // namespace utils
232 
getVersion(const std::string & psuInventoryPath)233 std::string getVersion(const std::string& psuInventoryPath)
234 {
235     const auto& [devicePath, type, versionStr] =
236         utils::getVersionInfo(psuInventoryPath);
237     if (devicePath.empty() || versionStr.empty())
238     {
239         return "";
240     }
241     std::string version;
242     try
243     {
244         phosphor::pmbus::PMBus pmbus(devicePath);
245         version = pmbus.readString(versionStr, type);
246     }
247     catch (const std::exception& ex)
248     {
249         log<level::ERR>(ex.what());
250     }
251     return version;
252 }
253 
getVersion(sdbusplus::bus_t & bus,const std::string & psuInventoryPath)254 std::string getVersion(sdbusplus::bus_t& bus,
255                        const std::string& psuInventoryPath)
256 {
257     try
258     {
259         constexpr auto FW_VERSION = "fw_version";
260         using namespace phosphor::pmbus;
261         const auto IBMCFFPS_FW_VERSION_SIZE = 12;
262         const auto& [i2cbus, i2caddr] = utils::getPsuI2c(bus, psuInventoryPath);
263 
264         auto pmbusIntf = utils::getPmbusIntf(i2cbus, i2caddr);
265 
266         if (!pmbusIntf)
267         {
268             log<level::WARNING>("Unable to get pointer PMBus Interface");
269             return "";
270         }
271 
272         std::string fwVersion =
273             utils::readVPDValue(*pmbusIntf, FW_VERSION, Type::HwmonDeviceDebug,
274                                 IBMCFFPS_FW_VERSION_SIZE);
275         return fwVersion;
276     }
277     catch (const std::exception& e)
278     {
279         log<level::ERR>(std::format("Error: {}", e.what()).c_str());
280         return "";
281     }
282 }
283 
getLatest(const std::vector<std::string> & versions)284 std::string getLatest(const std::vector<std::string>& versions)
285 {
286     // TODO: when multiple PSU/Machines are supported, add configuration options
287     // to implement machine-specific logic.
288     // For now IBM AC Servers and Inspur FP5280G2 are supported.
289     //
290     // IBM AC servers' PSU version has two types:
291     // * XXXXYYYYZZZZ: XXXX is the primary version
292     //                 YYYY is the secondary version
293     //                 ZZZZ is the communication version
294     //
295     // * XXXXYYYY:     XXXX is the primary version
296     //                 YYYY is the seconday version
297     //
298     // Inspur FP5280G2 PSU version is human readable text and a larger string
299     // means a newer version.
300     //
301     // So just compare by strings is OK for these cases
302     return utils::getLatestDefault(versions);
303 }
304 } // namespace version
305