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