xref: /openbmc/phosphor-fan-presence/monitor/fan_error.cpp (revision a00f683cea5d288e2996a4b20abe3fb20d59f716)
1 /**
2  * Copyright © 2022 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 "fan_error.hpp"
17 
18 #include "logging.hpp"
19 #include "sdbusplus.hpp"
20 
21 #include <systemd/sd-journal.h>
22 
23 #include <nlohmann/json.hpp>
24 #include <phosphor-logging/lg2.hpp>
25 #include <xyz/openbmc_project/Logging/Create/server.hpp>
26 
27 #include <filesystem>
28 
29 namespace phosphor::fan::monitor
30 {
31 
32 using FFDCFormat =
33     sdbusplus::xyz::openbmc_project::Logging::server::Create::FFDCFormat;
34 using FFDCFiles = std::vector<
35     std::tuple<FFDCFormat, uint8_t, uint8_t, sdbusplus::message::unix_fd>>;
36 using json = nlohmann::json;
37 
38 const auto loggingService = "xyz.openbmc_project.Logging";
39 const auto loggingPath = "/xyz/openbmc_project/logging";
40 const auto loggingCreateIface = "xyz.openbmc_project.Logging.Create";
41 
42 namespace fs = std::filesystem;
43 using namespace phosphor::fan::util;
44 
45 /**
46  * @class JournalCloser
47  *
48  * Automatically closes the journal when the object goes out of scope.
49  */
50 class JournalCloser
51 {
52   public:
53     // Specify which compiler-generated methods we want
54     JournalCloser() = delete;
55     JournalCloser(const JournalCloser&) = delete;
56     JournalCloser(JournalCloser&&) = delete;
57     JournalCloser& operator=(const JournalCloser&) = delete;
58     JournalCloser& operator=(JournalCloser&&) = delete;
59 
JournalCloser(sd_journal * journal)60     explicit JournalCloser(sd_journal* journal) : journal{journal} {}
61 
~JournalCloser()62     ~JournalCloser()
63     {
64         sd_journal_close(journal);
65     }
66 
67   private:
68     sd_journal* journal{nullptr};
69 };
70 
FFDCFile(const fs::path & name)71 FFDCFile::FFDCFile(const fs::path& name) :
72     _fd(open(name.c_str(), O_RDONLY)), _name(name)
73 {
74     if (_fd() == -1)
75     {
76         auto e = errno;
77         getLogger().log(std::format("Could not open FFDC file {}. errno {}",
78                                     _name.string(), e));
79     }
80 }
81 
commit(const json & jsonFFDC,bool isPowerOffError)82 void FanError::commit(const json& jsonFFDC, bool isPowerOffError)
83 {
84     FFDCFiles ffdc;
85     auto ad = getAdditionalData(isPowerOffError);
86 
87     // Add the Logger contents as FFDC
88     auto logFile = makeLogFFDCFile();
89     if (logFile && (logFile->fd() != -1))
90     {
91         ffdc.emplace_back(FFDCFormat::Text, 0x01, 0x01, logFile->fd());
92     }
93 
94     // Add the passed in JSON as FFDC
95     auto ffdcFile = makeJsonFFDCFile(jsonFFDC);
96     if (ffdcFile && (ffdcFile->fd() != -1))
97     {
98         ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, ffdcFile->fd());
99     }
100 
101     // add the previous systemd journal entries as FFDC
102     auto serviceFFDC = makeJsonFFDCFile(getJournalEntries(25));
103     if (serviceFFDC && serviceFFDC->fd() != -1)
104     {
105         ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, serviceFFDC->fd());
106     }
107 
108     try
109     {
110         auto sev = _severity;
111 
112         // If this is a power off, change severity to Critical
113         if (isPowerOffError)
114         {
115             using namespace sdbusplus::xyz::openbmc_project::Logging::server;
116             sev = convertForMessage(Entry::Level::Critical);
117         }
118         SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface,
119                               "CreateWithFFDCFiles", _errorName, sev, ad, ffdc);
120     }
121     catch (const DBusError& e)
122     {
123         getLogger().log(
124             std::format("Call to create a {} error for fan {} failed: {}",
125                         _errorName, _fanName, e.what()),
126             Logger::error);
127     }
128 }
129 
getAdditionalData(bool isPowerOffError)130 std::map<std::string, std::string> FanError::getAdditionalData(
131     bool isPowerOffError)
132 {
133     std::map<std::string, std::string> ad;
134 
135     ad.emplace("_PID", std::to_string(getpid()));
136 
137     if (!_fanName.empty())
138     {
139         ad.emplace("CALLOUT_INVENTORY_PATH", _fanName);
140     }
141 
142     if (!_sensorName.empty())
143     {
144         ad.emplace("FAN_SENSOR", _sensorName);
145     }
146 
147     // If this is a power off, specify that it's a power
148     // fault and a system termination.  This is used by some
149     // implementations for service reasons.
150     if (isPowerOffError)
151     {
152         ad.emplace("SEVERITY_DETAIL", "SYSTEM_TERM");
153     }
154 
155     return ad;
156 }
157 
makeLogFFDCFile()158 std::unique_ptr<FFDCFile> FanError::makeLogFFDCFile()
159 {
160     try
161     {
162         auto logFile = getLogger().saveToTempFile();
163         return std::make_unique<FFDCFile>(logFile);
164     }
165     catch (const std::exception& e)
166     {
167         lg2::error("Could not save log contents in FFDC. Error msg: {ERROR}",
168                    "ERROR", e);
169     }
170     return nullptr;
171 }
172 
makeJsonFFDCFile(const json & ffdcData)173 std::unique_ptr<FFDCFile> FanError::makeJsonFFDCFile(const json& ffdcData)
174 {
175     char tmpFile[] = "/tmp/fanffdc.XXXXXX";
176     auto fd = mkstemp(tmpFile);
177     if (fd != -1)
178     {
179         auto jsonString = ffdcData.dump();
180 
181         auto rc = write(fd, jsonString.data(), jsonString.size());
182         close(fd);
183         if (rc != -1)
184         {
185             fs::path jsonFile{tmpFile};
186             return std::make_unique<FFDCFile>(jsonFile);
187         }
188         else
189         {
190             getLogger().log("Failed call to write JSON FFDC file");
191         }
192     }
193     else
194     {
195         auto e = errno;
196         getLogger().log(std::format("Failed called to mkstemp, errno = {}", e),
197                         Logger::error);
198     }
199     return nullptr;
200 }
201 
getJournalEntries(int numLines) const202 nlohmann::json FanError::getJournalEntries(int numLines) const
203 {
204     // Sleep 100ms; otherwise recent journal entries sometimes not available
205     using namespace std::chrono_literals;
206     std::this_thread::sleep_for(100ms);
207 
208     std::vector<std::string> entries;
209 
210     // Open the journal
211     sd_journal* journal;
212     int rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY);
213     if (rc < 0)
214     {
215         // Build one line string containing field values
216         entries.push_back("[Internal error: sd_journal_open(), rc=" +
217                           std::string(strerror(rc)) + "]");
218         return json(entries);
219     }
220 
221     // Create object to automatically close journal
222     JournalCloser closer{journal};
223 
224     std::string field{"SYSLOG_IDENTIFIER"};
225     std::vector<std::string> executables{"systemd"};
226 
227     entries.reserve(2 * numLines);
228 
229     for (const auto& executable : executables)
230     {
231         // Add match so we only loop over entries with specified field value
232         std::string match{field + '=' + executable};
233         rc = sd_journal_add_match(journal, match.c_str(), 0);
234         if (rc < 0)
235         {
236             // Build one line string containing field values
237             entries.push_back("[Internal error: sd_journal_add_match(), rc=" +
238                               std::string(strerror(rc)) + "]");
239 
240             break;
241         }
242 
243         int count{1};
244 
245         std::string syslogID, pid, message, timeStamp;
246 
247         // Loop through journal entries from newest to oldest
248         SD_JOURNAL_FOREACH_BACKWARDS(journal)
249         {
250             // Get relevant journal entry fields
251             timeStamp = getTimeStamp(journal);
252             syslogID = getFieldValue(journal, "SYSLOG_IDENTIFIER");
253             pid = getFieldValue(journal, "_PID");
254             message = getFieldValue(journal, "MESSAGE");
255 
256             // Build one line string containing field values
257             entries.push_back(
258                 timeStamp + " " + syslogID + "[" + pid + "]: " + message);
259 
260             // Stop after number of lines was read
261             if (count++ >= numLines)
262             {
263                 break;
264             }
265         }
266     }
267 
268     // put the journal entries in chronological order
269     std::reverse(entries.begin(), entries.end());
270 
271     return json(entries);
272 }
273 
getTimeStamp(sd_journal * journal) const274 std::string FanError::getTimeStamp(sd_journal* journal) const
275 {
276     // Get realtime (wallclock) timestamp of current journal entry.  The
277     // timestamp is in microseconds since the epoch.
278     uint64_t usec{0};
279     int rc = sd_journal_get_realtime_usec(journal, &usec);
280     if (rc < 0)
281     {
282         return "[Internal error: sd_journal_get_realtime_usec(), rc=" +
283                std::string(strerror(rc)) + "]";
284     }
285 
286     // Convert to number of seconds since the epoch
287     time_t secs = usec / 1000000;
288 
289     // Convert seconds to tm struct required by strftime()
290     struct tm* timeStruct = localtime(&secs);
291     if (timeStruct == nullptr)
292     {
293         return "[Internal error: localtime() returned nullptr]";
294     }
295 
296     // Convert tm struct into a date/time string
297     char timeStamp[80];
298     strftime(timeStamp, sizeof(timeStamp), "%b %d %H:%M:%S", timeStruct);
299 
300     return timeStamp;
301 }
302 
getFieldValue(sd_journal * journal,const std::string & field) const303 std::string FanError::getFieldValue(sd_journal* journal,
304                                     const std::string& field) const
305 {
306     std::string value{};
307 
308     // Get field data from current journal entry
309     const void* data{nullptr};
310     size_t length{0};
311     int rc = sd_journal_get_data(journal, field.c_str(), &data, &length);
312     if (rc < 0)
313     {
314         if (-rc == ENOENT)
315         {
316             // Current entry does not include this field; return empty value
317             return value;
318         }
319         else
320         {
321             return "[Internal error: sd_journal_get_data() rc=" +
322                    std::string(strerror(rc)) + "]";
323         }
324     }
325 
326     // Get value from field data.  Field data in format "FIELD=value".
327     std::string dataString{static_cast<const char*>(data), length};
328     std::string::size_type pos = dataString.find('=');
329     if ((pos != std::string::npos) && ((pos + 1) < dataString.size()))
330     {
331         // Value is substring after the '='
332         value = dataString.substr(pos + 1);
333     }
334 
335     return value;
336 }
337 
338 } // namespace phosphor::fan::monitor
339