1 #include "ipmi_fru_info_area.hpp"
2 
3 #include <phosphor-logging/elog.hpp>
4 
5 #include <algorithm>
6 #include <ctime>
7 #include <iomanip>
8 #include <map>
9 #include <numeric>
10 #include <sstream>
11 
12 namespace ipmi
13 {
14 namespace fru
15 {
16 using namespace phosphor::logging;
17 
18 // Property variables
19 static constexpr auto partNumber = "Part Number";
20 static constexpr auto serialNumber = "Serial Number";
21 static constexpr auto manufacturer = "Manufacturer";
22 static constexpr auto buildDate = "Mfg Date";
23 static constexpr auto modelNumber = "Model Number";
24 static constexpr auto prettyName = "Name";
25 static constexpr auto version = "Version";
26 static constexpr auto type = "Type";
27 
28 // Board info areas
29 static constexpr auto board = "Board";
30 static constexpr auto chassis = "Chassis";
31 static constexpr auto product = "Product";
32 
33 static constexpr auto specVersion = 0x1;
34 static constexpr auto recordUnitOfMeasurement = 0x8; // size in bytes
35 static constexpr auto checksumSize = 0x1;            // size in bytes
36 static constexpr auto recordNotPresent = 0x0;
37 static constexpr auto englishLanguageCode = 0x0;
38 static constexpr auto typeLengthByteNull = 0x0;
39 static constexpr auto endOfCustomFields = 0xC1;
40 static constexpr auto commonHeaderFormatSize = 0x8; // size in bytes
41 static constexpr auto manufacturingDateSize = 0x3;
42 static constexpr auto areaSizeOffset = 0x1;
43 static constexpr uint8_t typeASCII = 0xC0;
44 static constexpr auto maxRecordAttributeValue = 0x3F;
45 
46 static constexpr auto secs_from_1970_1996 = 820454400;
47 static constexpr auto maxMfgDateValue = 0xFFFFFF; // 3 Byte length
48 static constexpr auto secs_per_min = 60;
49 static constexpr auto secsToMaxMfgdate = secs_from_1970_1996 +
50                                          secs_per_min * maxMfgDateValue;
51 
52 // Minimum size of resulting FRU blob.
53 // This is also the theoretical maximum size according to the spec:
54 // 8 bytes header + 5 areas at 0xff*8 bytes max each
55 // 8 + 5*0xff*8 = 0x27e0
56 static constexpr auto fruMinSize = 0x27E0;
57 
58 // Value to use for padding.
59 // Using 0xff to match the default (blank) value in a physical EEPROM.
60 static constexpr auto fruPadValue = 0xff;
61 
62 /**
63  * @brief Format Beginning of Individual IPMI FRU Data Section
64  *
65  * @param[in] langCode Language code
66  * @param[in/out] data FRU area data
67  */
preFormatProcessing(bool langCode,FruAreaData & data)68 void preFormatProcessing(bool langCode, FruAreaData& data)
69 {
70     // Add id for version of FRU Info Storage Spec used
71     data.emplace_back(specVersion);
72 
73     // Add Data Size - 0 as a placeholder, can edit after the data is finalized
74     data.emplace_back(typeLengthByteNull);
75 
76     if (langCode)
77     {
78         data.emplace_back(englishLanguageCode);
79     }
80 }
81 
82 /**
83  * @brief Append checksum of the FRU area data
84  *
85  * @param[in/out] data FRU area data
86  */
appendDataChecksum(FruAreaData & data)87 void appendDataChecksum(FruAreaData& data)
88 {
89     uint8_t checksumVal = std::accumulate(data.begin(), data.end(), 0);
90     // Push the Zero checksum as the last byte of this data
91     // This appears to be a simple summation of all the bytes
92     data.emplace_back(-checksumVal);
93 }
94 
95 /**
96  * @brief Append padding bytes for the FRU area data
97  *
98  * @param[in/out] data FRU area data
99  */
padData(FruAreaData & data)100 void padData(FruAreaData& data)
101 {
102     uint8_t pad = (data.size() + checksumSize) % recordUnitOfMeasurement;
103     if (pad)
104     {
105         data.resize((data.size() + recordUnitOfMeasurement - pad));
106     }
107 }
108 
109 /**
110  * @brief Format End of Individual IPMI FRU Data Section
111  *
112  * @param[in/out] fruAreaData FRU area info data
113  */
postFormatProcessing(FruAreaData & data)114 void postFormatProcessing(FruAreaData& data)
115 {
116     // This area needs to be padded to a multiple of 8 bytes (after checksum)
117     padData(data);
118 
119     // Set size of data info area
120     data.at(areaSizeOffset) = (data.size() + checksumSize) /
121                               (recordUnitOfMeasurement);
122 
123     // Finally add area checksum
124     appendDataChecksum(data);
125 }
126 
127 /**
128  * @brief Read chassis type property value from inventory and append to the FRU
129  * area data.
130  *
131  * @param[in] propMap map of property values
132  * @param[in,out] data FRU area data to be appended
133  */
appendChassisType(const PropertyMap & propMap,FruAreaData & data)134 void appendChassisType(const PropertyMap& propMap, FruAreaData& data)
135 {
136     uint8_t chassisType = 0; // Not specified
137     auto iter = propMap.find(type);
138     if (iter != propMap.end())
139     {
140         auto value = iter->second;
141         try
142         {
143             chassisType = std::stoi(value);
144         }
145         catch (const std::exception& e)
146         {
147             log<level::ERR>("Could not parse chassis type",
148                             entry("VALUE=%s", value.c_str()),
149                             entry("ERROR=%s", e.what()));
150             chassisType = 0;
151         }
152     }
153     data.emplace_back(chassisType);
154 }
155 
156 /**
157  * @brief Read property value from inventory and append to the FRU area data
158  *
159  * @param[in] key key to search for in the property inventory data
160  * @param[in] propMap map of property values
161  * @param[in,out] data FRU area data to be appended
162  */
appendData(const Property & key,const PropertyMap & propMap,FruAreaData & data)163 void appendData(const Property& key, const PropertyMap& propMap,
164                 FruAreaData& data)
165 {
166     auto iter = propMap.find(key);
167     if (iter != propMap.end())
168     {
169         auto value = iter->second;
170         // If starts with 0x or 0X remove them
171         // ex: 0x123a just take 123a
172         if ((value.compare(0, 2, "0x")) == 0 ||
173             (value.compare(0, 2, "0X") == 0))
174         {
175             value.erase(0, 2);
176         }
177 
178         // 6 bits for length as per FRU spec v1.0
179         // if length is greater then 63(2^6) bytes then trim the data to 63
180         // bytess.
181         auto valueLength = (value.length() > maxRecordAttributeValue)
182                                ? maxRecordAttributeValue
183                                : value.length();
184         // 2 bits for type
185         // Set the type to ascii
186         uint8_t typeLength = valueLength | ipmi::fru::typeASCII;
187 
188         data.emplace_back(typeLength);
189         std::copy(value.begin(), value.begin() + valueLength,
190                   std::back_inserter(data));
191     }
192     else
193     {
194         // set 0 size
195         data.emplace_back(typeLengthByteNull);
196     }
197 }
198 
timeStringToRaw(const std::string & input)199 std::time_t timeStringToRaw(const std::string& input)
200 {
201     // TODO: For non-US region timestamps, pass in region information for the
202     // FRU to avoid the month/day swap.
203     // 2017-02-24 - 13:59:00, Tue Nov 20 23:08:00 2018
204     static const std::vector<std::string> patterns = {"%Y-%m-%d - %H:%M:%S",
205                                                       "%a %b %d %H:%M:%S %Y"};
206 
207     std::tm time = {};
208 
209     for (const auto& pattern : patterns)
210     {
211         std::istringstream timeStream(input);
212         timeStream >> std::get_time(&time, pattern.c_str());
213         if (!timeStream.fail())
214         {
215             break;
216         }
217     }
218 
219     return timegm(&time);
220 }
221 
222 /**
223  * @brief Appends Build Date
224  *
225  * @param[in] propMap map of property values
226  * @param[in/out] data FRU area to add the manfufacture date
227  */
appendMfgDate(const PropertyMap & propMap,FruAreaData & data)228 void appendMfgDate(const PropertyMap& propMap, FruAreaData& data)
229 {
230     // MFG Date/Time
231     auto iter = propMap.find(buildDate);
232     if ((iter != propMap.end()) && (iter->second.size() > 0))
233     {
234         std::time_t raw = timeStringToRaw(iter->second);
235 
236         // From FRU Spec:
237         // "Mfg. Date / Time
238         // Number of minutes from 0:00 hrs 1/1/96.
239         // LSbyte first (little endian)
240         // 00_00_00h = unspecified."
241         if ((raw >= secs_from_1970_1996) && (raw <= secsToMaxMfgdate))
242         {
243             raw -= secs_from_1970_1996;
244             raw /= secs_per_min;
245             uint8_t fru_raw[3];
246             fru_raw[0] = raw & 0xFF;
247             fru_raw[1] = (raw >> 8) & 0xFF;
248             fru_raw[2] = (raw >> 16) & 0xFF;
249             std::copy(fru_raw, fru_raw + 3, std::back_inserter(data));
250             return;
251         }
252         std::fprintf(stderr, "MgfDate invalid date: %u secs since UNIX epoch\n",
253                      static_cast<unsigned int>(raw));
254     }
255     // Blank date
256     data.emplace_back(0);
257     data.emplace_back(0);
258     data.emplace_back(0);
259 }
260 
261 /**
262  * @brief Builds a section of the common header
263  *
264  * @param[in] infoAreaSize size of the FRU area to write
265  * @param[in] offset Current offset for data in overall record
266  * @param[in/out] data Common Header section data container
267  */
buildCommonHeaderSection(const uint32_t & infoAreaSize,uint16_t & offset,FruAreaData & data)268 void buildCommonHeaderSection(const uint32_t& infoAreaSize, uint16_t& offset,
269                               FruAreaData& data)
270 {
271     // Check if data for internal use section populated
272     if (infoAreaSize == 0)
273     {
274         // Indicate record not present
275         data.emplace_back(recordNotPresent);
276     }
277     else
278     {
279         // offset should be multiple of 8.
280         auto remainder = offset % recordUnitOfMeasurement;
281         // add the padding bytes in the offset so that offset
282         // will be multiple of 8 byte.
283         offset += (remainder > 0) ? recordUnitOfMeasurement - remainder : 0;
284         // Place data to define offset to area data section
285         data.emplace_back(offset / recordUnitOfMeasurement);
286 
287         offset += infoAreaSize;
288     }
289 }
290 
291 /**
292  * @brief Builds the Chassis info area data section
293  *
294  * @param[in] propMap map of properties for chassis info area
295  * @return FruAreaData container with chassis info area
296  */
buildChassisInfoArea(const PropertyMap & propMap)297 FruAreaData buildChassisInfoArea(const PropertyMap& propMap)
298 {
299     FruAreaData fruAreaData;
300     if (!propMap.empty())
301     {
302         // Set formatting data that goes at the beginning of the record
303         preFormatProcessing(false, fruAreaData);
304 
305         // chassis type
306         appendChassisType(propMap, fruAreaData);
307 
308         // Chasiss part number, in config.yaml it is configured as model
309         appendData(modelNumber, propMap, fruAreaData);
310 
311         // Board serial number
312         appendData(serialNumber, propMap, fruAreaData);
313 
314         // Indicate End of Custom Fields
315         fruAreaData.emplace_back(endOfCustomFields);
316 
317         // Complete record data formatting
318         postFormatProcessing(fruAreaData);
319     }
320     return fruAreaData;
321 }
322 
323 /**
324  * @brief Builds the Board info area data section
325  *
326  * @param[in] propMap map of properties for board info area
327  * @return FruAreaData container with board info area
328  */
buildBoardInfoArea(const PropertyMap & propMap)329 FruAreaData buildBoardInfoArea(const PropertyMap& propMap)
330 {
331     FruAreaData fruAreaData;
332     if (!propMap.empty())
333     {
334         preFormatProcessing(true, fruAreaData);
335 
336         // Manufacturing date
337         appendMfgDate(propMap, fruAreaData);
338 
339         // manufacturer
340         appendData(manufacturer, propMap, fruAreaData);
341 
342         // Product name/Pretty name
343         appendData(prettyName, propMap, fruAreaData);
344 
345         // Board serial number
346         appendData(serialNumber, propMap, fruAreaData);
347 
348         // Board part number
349         appendData(partNumber, propMap, fruAreaData);
350 
351         // FRU File ID - Empty
352         fruAreaData.emplace_back(typeLengthByteNull);
353 
354         // Empty FRU File ID bytes
355         fruAreaData.emplace_back(recordNotPresent);
356 
357         // End of custom fields
358         fruAreaData.emplace_back(endOfCustomFields);
359 
360         postFormatProcessing(fruAreaData);
361     }
362     return fruAreaData;
363 }
364 
365 /**
366  * @brief Builds the Product info area data section
367  *
368  * @param[in] propMap map of FRU properties for Board info area
369  * @return FruAreaData container with product info area data
370  */
buildProductInfoArea(const PropertyMap & propMap)371 FruAreaData buildProductInfoArea(const PropertyMap& propMap)
372 {
373     FruAreaData fruAreaData;
374     if (!propMap.empty())
375     {
376         // Set formatting data that goes at the beginning of the record
377         preFormatProcessing(true, fruAreaData);
378 
379         // manufacturer
380         appendData(manufacturer, propMap, fruAreaData);
381 
382         // Product name/Pretty name
383         appendData(prettyName, propMap, fruAreaData);
384 
385         // Product part/model number
386         appendData(modelNumber, propMap, fruAreaData);
387 
388         // Product version
389         appendData(version, propMap, fruAreaData);
390 
391         // Serial Number
392         appendData(serialNumber, propMap, fruAreaData);
393 
394         // Add Asset Tag
395         fruAreaData.emplace_back(recordNotPresent);
396 
397         // FRU File ID - Empty
398         fruAreaData.emplace_back(typeLengthByteNull);
399 
400         // Empty FRU File ID bytes
401         fruAreaData.emplace_back(recordNotPresent);
402 
403         // End of custom fields
404         fruAreaData.emplace_back(endOfCustomFields);
405 
406         postFormatProcessing(fruAreaData);
407     }
408     return fruAreaData;
409 }
410 
buildFruAreaData(const FruInventoryData & inventory)411 FruAreaData buildFruAreaData(const FruInventoryData& inventory)
412 {
413     FruAreaData combFruArea{};
414     // Now build common header with data for this FRU Inv Record
415     // Use this variable to increment size of header as we go along to determine
416     // offset for the subsequent area offsets
417     uint16_t curDataOffset = commonHeaderFormatSize;
418     // First byte is id for version of FRU Info Storage Spec used
419     combFruArea.emplace_back(specVersion);
420 
421     // 2nd byte is offset to internal use data
422     combFruArea.emplace_back(recordNotPresent);
423 
424     // 3rd byte is offset to chassis data
425     FruAreaData chassisArea;
426     auto chassisIt = inventory.find(chassis);
427     if (chassisIt != inventory.end())
428     {
429         chassisArea = buildChassisInfoArea(chassisIt->second);
430     }
431     // update the offset to chassis data.
432     buildCommonHeaderSection(chassisArea.size(), curDataOffset, combFruArea);
433 
434     // 4th byte is offset to board data
435     FruAreaData boardArea;
436     auto boardIt = inventory.find(board);
437     if (boardIt != inventory.end())
438     {
439         boardArea = buildBoardInfoArea(boardIt->second);
440     }
441     // update the offset to the board data.
442     buildCommonHeaderSection(boardArea.size(), curDataOffset, combFruArea);
443 
444     // 5th byte is offset to product data
445     FruAreaData prodArea;
446     auto prodIt = inventory.find(product);
447     if (prodIt != inventory.end())
448     {
449         prodArea = buildProductInfoArea(prodIt->second);
450     }
451     // update the offset to the product data.
452     buildCommonHeaderSection(prodArea.size(), curDataOffset, combFruArea);
453 
454     // 6th byte is offset to multirecord data
455     combFruArea.emplace_back(recordNotPresent);
456 
457     // 7th byte is PAD
458     combFruArea.emplace_back(recordNotPresent);
459 
460     // 8th (Final byte of Header Format) is the checksum
461     appendDataChecksum(combFruArea);
462 
463     // Combine everything into one full IPMI FRU specification Record
464     // add chassis use area data
465     combFruArea.insert(combFruArea.end(), chassisArea.begin(),
466                        chassisArea.end());
467 
468     // add board area data
469     combFruArea.insert(combFruArea.end(), boardArea.begin(), boardArea.end());
470 
471     // add product use area data
472     combFruArea.insert(combFruArea.end(), prodArea.begin(), prodArea.end());
473 
474     // If area is smaller than the minimum size, pad it. This enables ipmitool
475     // to update the FRU blob with values longer than the original payload.
476     if (combFruArea.size() < fruMinSize)
477     {
478         combFruArea.resize(fruMinSize, fruPadValue);
479     }
480 
481     return combFruArea;
482 }
483 
484 } // namespace fru
485 } // namespace ipmi
486