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 "registry.hpp"
17 
18 #include "json_utils.hpp"
19 #include "pel_types.hpp"
20 #include "pel_values.hpp"
21 
22 #include <phosphor-logging/log.hpp>
23 
24 #include <fstream>
25 
26 namespace openpower
27 {
28 namespace pels
29 {
30 namespace message
31 {
32 
33 namespace pv = pel_values;
34 namespace fs = std::filesystem;
35 using namespace phosphor::logging;
36 
37 constexpr auto debugFilePath = "/etc/phosphor-logging/";
38 
39 namespace helper
40 {
41 
42 uint8_t getSubsystem(const std::string& subsystemName)
43 {
44     // Get the actual value to use in the PEL for the string name
45     auto ss = pv::findByName(subsystemName, pv::subsystemValues);
46     if (ss == pv::subsystemValues.end())
47     {
48         // Schema validation should be catching this.
49         log<level::ERR>("Invalid subsystem name used in message registry",
50                         entry("SUBSYSTEM=%s", subsystemName.c_str()));
51 
52         throw std::runtime_error("Invalid subsystem used in message registry");
53     }
54 
55     return std::get<pv::fieldValuePos>(*ss);
56 }
57 
58 uint8_t getSeverity(const std::string& severityName)
59 {
60     auto s = pv::findByName(severityName, pv::severityValues);
61     if (s == pv::severityValues.end())
62     {
63         // Schema validation should be catching this.
64         log<level::ERR>("Invalid severity name used in message registry",
65                         entry("SEVERITY=%s", severityName.c_str()));
66 
67         throw std::runtime_error("Invalid severity used in message registry");
68     }
69 
70     return std::get<pv::fieldValuePos>(*s);
71 }
72 
73 std::vector<RegistrySeverity> getSeverities(const nlohmann::json& severity)
74 {
75     std::vector<RegistrySeverity> severities;
76 
77     // The plain string value, like "unrecoverable"
78     if (severity.is_string())
79     {
80         RegistrySeverity s;
81         s.severity = getSeverity(severity.get<std::string>());
82         severities.push_back(std::move(s));
83     }
84     else
85     {
86         // An array, with an element like:
87         // {
88         //    "SevValue": "unrecoverable",
89         //    "System", "systemA"
90         // }
91         for (const auto& sev : severity)
92         {
93             RegistrySeverity s;
94             s.severity = getSeverity(sev["SevValue"].get<std::string>());
95 
96             if (sev.contains("System"))
97             {
98                 s.system = sev["System"].get<std::string>();
99             }
100 
101             severities.push_back(std::move(s));
102         }
103     }
104 
105     return severities;
106 }
107 
108 uint16_t getActionFlags(const std::vector<std::string>& flags)
109 {
110     uint16_t actionFlags = 0;
111 
112     // Make the bitmask based on the array of flag names
113     for (const auto& flag : flags)
114     {
115         auto s = pv::findByName(flag, pv::actionFlagsValues);
116         if (s == pv::actionFlagsValues.end())
117         {
118             // Schema validation should be catching this.
119             log<level::ERR>("Invalid action flag name used in message registry",
120                             entry("FLAG=%s", flag.c_str()));
121 
122             throw std::runtime_error(
123                 "Invalid action flag used in message registry");
124         }
125 
126         actionFlags |= std::get<pv::fieldValuePos>(*s);
127     }
128 
129     return actionFlags;
130 }
131 
132 uint8_t getEventType(const std::string& eventTypeName)
133 {
134     auto t = pv::findByName(eventTypeName, pv::eventTypeValues);
135     if (t == pv::eventTypeValues.end())
136     {
137         log<level::ERR>("Invalid event type used in message registry",
138                         entry("EVENT_TYPE=%s", eventTypeName.c_str()));
139 
140         throw std::runtime_error("Invalid event type used in message registry");
141     }
142     return std::get<pv::fieldValuePos>(*t);
143 }
144 
145 uint8_t getEventScope(const std::string& eventScopeName)
146 {
147     auto s = pv::findByName(eventScopeName, pv::eventScopeValues);
148     if (s == pv::eventScopeValues.end())
149     {
150         log<level::ERR>("Invalid event scope used in registry",
151                         entry("EVENT_SCOPE=%s", eventScopeName.c_str()));
152 
153         throw std::runtime_error(
154             "Invalid event scope used in message registry");
155     }
156     return std::get<pv::fieldValuePos>(*s);
157 }
158 
159 uint16_t getSRCReasonCode(const nlohmann::json& src, const std::string& name)
160 {
161     std::string rc = src["ReasonCode"];
162     uint16_t reasonCode = strtoul(rc.c_str(), nullptr, 16);
163     if (reasonCode == 0)
164     {
165         log<phosphor::logging::level::ERR>(
166             "Invalid reason code in message registry",
167             entry("ERROR_NAME=%s", name.c_str()),
168             entry("REASON_CODE=%s", rc.c_str()));
169 
170         throw std::runtime_error("Invalid reason code in message registry");
171     }
172     return reasonCode;
173 }
174 
175 uint8_t getSRCType(const nlohmann::json& src, const std::string& name)
176 {
177     // Looks like: "22"
178     std::string srcType = src["Type"];
179     size_t type = strtoul(srcType.c_str(), nullptr, 16);
180     if ((type == 0) || (srcType.size() != 2)) // 1 hex byte
181     {
182         log<phosphor::logging::level::ERR>(
183             "Invalid SRC Type in message registry",
184             entry("ERROR_NAME=%s", name.c_str()),
185             entry("SRC_TYPE=%s", srcType.c_str()));
186 
187         throw std::runtime_error("Invalid SRC Type in message registry");
188     }
189 
190     return type;
191 }
192 
193 std::optional<std::map<SRC::WordNum, SRC::AdditionalDataField>>
194     getSRCHexwordFields(const nlohmann::json& src, const std::string& name)
195 {
196     std::map<SRC::WordNum, SRC::AdditionalDataField> hexwordFields;
197 
198     // Build the map of which AdditionalData fields to use for which SRC words
199 
200     // Like:
201     // {
202     //   "8":
203     //   {
204     //     "AdditionalDataPropSource": "TEST"
205     //   }
206     //
207     // }
208 
209     for (const auto& word : src["Words6To9"].items())
210     {
211         std::string num = word.key();
212         size_t wordNum = std::strtoul(num.c_str(), nullptr, 10);
213 
214         if (wordNum == 0)
215         {
216             log<phosphor::logging::level::ERR>(
217                 "Invalid SRC word number in message registry",
218                 entry("ERROR_NAME=%s", name.c_str()),
219                 entry("SRC_WORD_NUM=%s", num.c_str()));
220 
221             throw std::runtime_error("Invalid SRC word in message registry");
222         }
223 
224         auto attributes = word.value();
225 
226         // Use an empty string for the description if it does not exist.
227         auto itr = attributes.find("Description");
228         std::string desc = (attributes.end() != itr) ? *itr : "";
229 
230         std::tuple<std::string, std::string> adPropSourceDesc(
231             attributes["AdditionalDataPropSource"], desc);
232         hexwordFields[wordNum] = std::move(adPropSourceDesc);
233     }
234 
235     if (!hexwordFields.empty())
236     {
237         return hexwordFields;
238     }
239 
240     return std::nullopt;
241 }
242 std::optional<std::vector<SRC::WordNum>>
243     getSRCSymptomIDFields(const nlohmann::json& src, const std::string& name)
244 {
245     std::vector<SRC::WordNum> symptomIDFields;
246 
247     // Looks like:
248     // "SymptomIDFields": ["SRCWord3", "SRCWord6"],
249 
250     for (const std::string field : src["SymptomIDFields"])
251     {
252         // Just need the last digit off the end, e.g. SRCWord6.
253         // The schema enforces the format of these.
254         auto srcWordNum = field.substr(field.size() - 1);
255         size_t num = std::strtoul(srcWordNum.c_str(), nullptr, 10);
256         if (num == 0)
257         {
258             log<phosphor::logging::level::ERR>(
259                 "Invalid symptom ID field in message registry",
260                 entry("ERROR_NAME=%s", name.c_str()),
261                 entry("FIELD_NAME=%s", srcWordNum.c_str()));
262 
263             throw std::runtime_error("Invalid symptom ID in message registry");
264         }
265         symptomIDFields.push_back(num);
266     }
267     if (!symptomIDFields.empty())
268     {
269         return symptomIDFields;
270     }
271 
272     return std::nullopt;
273 }
274 
275 uint16_t getComponentID(uint8_t srcType, uint16_t reasonCode,
276                         const nlohmann::json& pelEntry, const std::string& name)
277 {
278     uint16_t id = 0;
279 
280     // If the ComponentID field is there, use that.  Otherwise, if it's a
281     // 0xBD BMC error SRC, use the reasoncode.
282     if (pelEntry.contains("ComponentID"))
283     {
284         std::string componentID = pelEntry["ComponentID"];
285         id = strtoul(componentID.c_str(), nullptr, 16);
286     }
287     else
288     {
289         // On BMC error SRCs (BD), can just get the component ID from
290         // the first byte of the reason code.
291         if (srcType == static_cast<uint8_t>(SRCType::bmcError))
292         {
293             id = reasonCode & 0xFF00;
294         }
295         else
296         {
297             log<level::ERR>("Missing component ID field in message registry",
298                             entry("ERROR_NAME=%s", name.c_str()));
299 
300             throw std::runtime_error(
301                 "Missing component ID field in message registry");
302         }
303     }
304 
305     return id;
306 }
307 
308 /**
309  * @brief Says if the JSON is the format that contains AdditionalData keys
310  *        as in index into them.
311  *
312  * @param[in] json - The highest level callout JSON
313  *
314  * @return bool - If it is the AdditionalData format or not
315  */
316 bool calloutUsesAdditionalData(const nlohmann::json& json)
317 {
318     return (json.contains("ADName") &&
319             json.contains("CalloutsWithTheirADValues"));
320 }
321 
322 /**
323  * @brief Finds the callouts to use when there is no AdditionalData,
324  *        but the system type may be used as a key.
325  *
326  * One entry in the array looks like the following.  The System key
327  * is optional and if not present it means that entry applies to
328  * every configuration that doesn't have another entry with a matching
329  * System key.
330  *
331  *    {
332  *        "System": "system1",
333  *        "CalloutList":
334  *        [
335  *            {
336  *                "Priority": "high",
337  *                "LocCode": "P1-C1"
338  *            },
339  *            {
340  *                "Priority": "low",
341  *                "LocCode": "P1"
342  *            }
343  *        ]
344  *    }
345  */
346 const nlohmann::json&
347     findCalloutList(const nlohmann::json& json,
348                     const std::vector<std::string>& systemNames)
349 {
350     const nlohmann::json* callouts = nullptr;
351 
352     if (!json.is_array())
353     {
354         throw std::runtime_error{"findCalloutList was not passed a JSON array"};
355     }
356 
357     // The entry with the system type match will take precedence over the entry
358     // without any "System" field in it at all, which will match all other
359     // cases.
360     for (const auto& calloutList : json)
361     {
362         if (calloutList.contains("System"))
363         {
364             if (std::find(systemNames.begin(), systemNames.end(),
365                           calloutList["System"].get<std::string>()) !=
366                 systemNames.end())
367             {
368                 callouts = &calloutList["CalloutList"];
369                 break;
370             }
371         }
372         else
373         {
374             // Any entry with no System key
375             callouts = &calloutList["CalloutList"];
376         }
377     }
378 
379     if (!callouts)
380     {
381         std::string types;
382         std::for_each(systemNames.begin(), systemNames.end(),
383                       [&types](const auto& t) { types += t + '|'; });
384         log<level::WARNING>(
385             "No matching system name entry or default system name entry "
386             " for PEL callout list",
387             entry("SYSTEMNAMES=%s", types.c_str()));
388 
389         throw std::runtime_error{
390             "Could not find a CalloutList JSON for this error and system name"};
391     }
392 
393     return *callouts;
394 }
395 
396 /**
397  * @brief Creates a RegistryCallout based on the input JSON.
398  *
399  * The JSON looks like:
400  *     {
401  *          "Priority": "high",
402  *          "LocCode": "E1"
403  *          ...
404  *     }
405  *
406  * Schema validation enforces what keys are present.
407  *
408  * @param[in] json - The JSON dictionary entry for a callout
409  *
410  * @return RegistryCallout - A filled in RegistryCallout
411  */
412 RegistryCallout makeRegistryCallout(const nlohmann::json& json)
413 {
414     RegistryCallout callout;
415 
416     callout.priority = "high";
417     callout.useInventoryLocCode = false;
418 
419     if (json.contains("Priority"))
420     {
421         callout.priority = json["Priority"].get<std::string>();
422     }
423 
424     if (json.contains("LocCode"))
425     {
426         callout.locCode = json["LocCode"].get<std::string>();
427     }
428 
429     if (json.contains("Procedure"))
430     {
431         callout.procedure = json["Procedure"].get<std::string>();
432     }
433     else if (json.contains("SymbolicFRU"))
434     {
435         callout.symbolicFRU = json["SymbolicFRU"].get<std::string>();
436     }
437     else if (json.contains("SymbolicFRUTrusted"))
438     {
439         callout.symbolicFRUTrusted =
440             json["SymbolicFRUTrusted"].get<std::string>();
441     }
442 
443     if (json.contains("UseInventoryLocCode"))
444     {
445         callout.useInventoryLocCode = json["UseInventoryLocCode"].get<bool>();
446     }
447 
448     return callout;
449 }
450 
451 /**
452  * @brief Returns the callouts to use when an AdditionalData key is
453  *        required to find the correct entries.
454  *
455  *       The System property is used to find which CalloutList to use.
456  *       If System is missing, then that CalloutList is valid for
457  *       everything.
458  *
459  * The JSON looks like:
460  *    [
461  *        {
462  *            "System": "systemA",
463  *            "CalloutList":
464  *            [
465  *                {
466  *                    "Priority": "high",
467  *                    "LocCode": "P1-C5"
468  *                }
469  *            ]
470  *         }
471  *    ]
472  *
473  * @param[in] json - The callout JSON
474  * @param[in] systemNames - List of compatible system type names
475  *
476  * @return std::vector<RegistryCallout> - The callouts to use
477  */
478 std::vector<RegistryCallout>
479     getCalloutsWithoutAD(const nlohmann::json& json,
480                          const std::vector<std::string>& systemNames)
481 {
482     std::vector<RegistryCallout> calloutEntries;
483 
484     // Find the CalloutList to use based on the system type
485     const auto& calloutList = findCalloutList(json, systemNames);
486 
487     // We finally found the callouts, make the objects.
488     for (const auto& callout : calloutList)
489     {
490         calloutEntries.push_back(std::move(makeRegistryCallout(callout)));
491     }
492 
493     return calloutEntries;
494 }
495 
496 /**
497  * @brief Returns the callouts to use when an AdditionalData key is
498  *        required to find the correct entries.
499  *
500  * The JSON looks like:
501  *    {
502  *        "ADName": "PROC_NUM",
503  *        "CalloutsWithTheirADValues":
504  *        [
505  *            {
506  *                "ADValue": "0",
507  *                "Callouts":
508  *                [
509  *                    {
510  *                        "CalloutList":
511  *                        [
512  *                            {
513  *                                "Priority": "high",
514  *                                "LocCode": "P1-C5"
515  *                            }
516  *                        ]
517  *                    }
518  *                ]
519  *            }
520  *        ]
521  *     }
522  *
523  * Note that the "Callouts" entry above is the same as the top level
524  * entry used when there is no AdditionalData key.
525  *
526  * @param[in] json - The callout JSON
527  * @param[in] systemNames - List of compatible system type names
528  * @param[in] additionalData - The AdditionalData property
529  *
530  * @return std::vector<RegistryCallout> - The callouts to use
531  */
532 std::vector<RegistryCallout>
533     getCalloutsUsingAD(const nlohmann::json& json,
534                        const std::vector<std::string>& systemNames,
535                        const AdditionalData& additionalData)
536 {
537     // This indicates which AD field we'll be using
538     auto keyName = json["ADName"].get<std::string>();
539 
540     // Get the actual value from the AD data
541     auto adValue = additionalData.getValue(keyName);
542 
543     if (!adValue)
544     {
545         // The AdditionalData did not contain the necessary key
546         log<level::WARNING>(
547             "The PEL message registry callouts JSON "
548             "said to use an AdditionalData key that isn't in the "
549             "AdditionalData event log property",
550             entry("ADNAME=%s\n", keyName.c_str()));
551         throw std::runtime_error{
552             "Missing AdditionalData entry for this callout"};
553     }
554 
555     const auto& callouts = json["CalloutsWithTheirADValues"];
556 
557     // find the entry with that AD value
558     auto it = std::find_if(
559         callouts.begin(), callouts.end(), [adValue](const nlohmann::json& j) {
560             return *adValue == j["ADValue"].get<std::string>();
561         });
562 
563     if (it == callouts.end())
564     {
565         // This can happen if not all possible values were in the
566         // message registry and that's fine.  There may be a
567         // "CalloutsWhenNoADMatch" section that contains callouts
568         // to use in this case.
569         if (json.contains("CalloutsWhenNoADMatch"))
570         {
571             return getCalloutsWithoutAD(json["CalloutsWhenNoADMatch"],
572                                         systemNames);
573         }
574         return std::vector<RegistryCallout>{};
575     }
576 
577     // Proceed to find the callouts possibly based on system type.
578     return getCalloutsWithoutAD((*it)["Callouts"], systemNames);
579 }
580 
581 } // namespace helper
582 
583 std::optional<Entry> Registry::lookup(const std::string& name, LookupType type,
584                                       bool toCache)
585 {
586     std::optional<nlohmann::json> registryTmp;
587     auto& registryOpt = (_registry) ? _registry : registryTmp;
588     if (!registryOpt)
589     {
590         registryOpt = readRegistry(_registryFile);
591         if (!registryOpt)
592         {
593             return std::nullopt;
594         }
595         else if (toCache)
596         {
597             // Save message registry in memory for peltool
598             _registry = std::move(registryTmp);
599         }
600     }
601     auto& reg = (_registry) ? _registry : registryTmp;
602     const auto& registry = reg.value();
603     // Find an entry with this name in the PEL array.
604     auto e = std::find_if(
605         registry["PELs"].begin(), registry["PELs"].end(),
606         [&name, &type](const auto& j) {
607             return ((name == j["Name"] && type == LookupType::name) ||
608                     (name == j["SRC"]["ReasonCode"] &&
609                      type == LookupType::reasonCode));
610         });
611 
612     if (e != registry["PELs"].end())
613     {
614         // Fill in the Entry structure from the JSON.  Most, but not all, fields
615         // are optional.
616 
617         try
618         {
619             Entry entry;
620             entry.name = (*e)["Name"];
621 
622             if (e->contains("Subsystem"))
623             {
624                 entry.subsystem = helper::getSubsystem((*e)["Subsystem"]);
625             }
626 
627             if (e->contains("ActionFlags"))
628             {
629                 entry.actionFlags = helper::getActionFlags((*e)["ActionFlags"]);
630             }
631 
632             if (e->contains("MfgActionFlags"))
633             {
634                 entry.mfgActionFlags =
635                     helper::getActionFlags((*e)["MfgActionFlags"]);
636             }
637 
638             if (e->contains("Severity"))
639             {
640                 entry.severity = helper::getSeverities((*e)["Severity"]);
641             }
642 
643             if (e->contains("MfgSeverity"))
644             {
645                 entry.mfgSeverity = helper::getSeverities((*e)["MfgSeverity"]);
646             }
647 
648             if (e->contains("EventType"))
649             {
650                 entry.eventType = helper::getEventType((*e)["EventType"]);
651             }
652 
653             if (e->contains("EventScope"))
654             {
655                 entry.eventScope = helper::getEventScope((*e)["EventScope"]);
656             }
657 
658             auto& src = (*e)["SRC"];
659             entry.src.reasonCode = helper::getSRCReasonCode(src, name);
660 
661             if (src.contains("Type"))
662             {
663                 entry.src.type = helper::getSRCType(src, name);
664             }
665             else
666             {
667                 entry.src.type = static_cast<uint8_t>(SRCType::bmcError);
668             }
669 
670             // Now that we know the SRC type and reason code,
671             // we can get the component ID.
672             entry.componentID = helper::getComponentID(
673                 entry.src.type, entry.src.reasonCode, *e, name);
674 
675             if (src.contains("Words6To9"))
676             {
677                 entry.src.hexwordADFields = helper::getSRCHexwordFields(src,
678                                                                         name);
679             }
680 
681             if (src.contains("SymptomIDFields"))
682             {
683                 entry.src.symptomID = helper::getSRCSymptomIDFields(src, name);
684             }
685 
686             auto& doc = (*e)["Documentation"];
687             entry.doc.message = doc["Message"];
688             entry.doc.description = doc["Description"];
689             if (doc.contains("MessageArgSources"))
690             {
691                 entry.doc.messageArgSources = doc["MessageArgSources"];
692             }
693 
694             // If there are callouts defined, save the JSON for later
695             if (_loadCallouts)
696             {
697                 if (e->contains("Callouts"))
698                 {
699                     entry.callouts = (*e)["Callouts"];
700                 }
701                 else if (e->contains("CalloutsUsingAD"))
702                 {
703                     entry.callouts = (*e)["CalloutsUsingAD"];
704                 }
705             }
706 
707             return entry;
708         }
709         catch (const std::exception& ex)
710         {
711             log<level::ERR>("Found invalid message registry field",
712                             entry("ERROR=%s", ex.what()));
713         }
714     }
715 
716     return std::nullopt;
717 }
718 
719 std::optional<nlohmann::json>
720     Registry::readRegistry(const std::filesystem::path& registryFile)
721 {
722     // Look in /etc first in case someone put a test file there
723     fs::path debugFile{fs::path{debugFilePath} / registryFileName};
724     nlohmann::json registry;
725     std::ifstream file;
726 
727     if (fs::exists(debugFile))
728     {
729         log<level::INFO>("Using debug PEL message registry");
730         file.open(debugFile);
731     }
732     else
733     {
734         file.open(registryFile);
735     }
736 
737     try
738     {
739         registry = nlohmann::json::parse(file);
740     }
741     catch (const std::exception& e)
742     {
743         log<level::ERR>("Error parsing message registry JSON",
744                         entry("JSON_ERROR=%s", e.what()));
745         return std::nullopt;
746     }
747     return registry;
748 }
749 
750 std::vector<RegistryCallout>
751     Registry::getCallouts(const nlohmann::json& calloutJSON,
752                           const std::vector<std::string>& systemNames,
753                           const AdditionalData& additionalData)
754 {
755     // The JSON may either use an AdditionalData key
756     // as an index, or not.
757     if (helper::calloutUsesAdditionalData(calloutJSON))
758     {
759         return helper::getCalloutsUsingAD(calloutJSON, systemNames,
760                                           additionalData);
761     }
762 
763     return helper::getCalloutsWithoutAD(calloutJSON, systemNames);
764 }
765 
766 } // namespace message
767 } // namespace pels
768 } // namespace openpower
769