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 <xyz/openbmc_project/Logging/Create/server.hpp>
25 
26 #include <filesystem>
27 
28 namespace phosphor::fan::monitor
29 {
30 
31 using FFDCFormat =
32     sdbusplus::xyz::openbmc_project::Logging::server::Create::FFDCFormat;
33 using FFDCFiles = std::vector<
34     std::tuple<FFDCFormat, uint8_t, uint8_t, sdbusplus::message::unix_fd>>;
35 using json = nlohmann::json;
36 
37 const auto loggingService = "xyz.openbmc_project.Logging";
38 const auto loggingPath = "/xyz/openbmc_project/logging";
39 const auto loggingCreateIface = "xyz.openbmc_project.Logging.Create";
40 
41 namespace fs = std::filesystem;
42 using namespace phosphor::fan::util;
43 
44 /**
45  * @class JournalCloser
46  *
47  * Automatically closes the journal when the object goes out of scope.
48  */
49 class JournalCloser
50 {
51   public:
52     // Specify which compiler-generated methods we want
53     JournalCloser() = delete;
54     JournalCloser(const JournalCloser&) = delete;
55     JournalCloser(JournalCloser&&) = delete;
56     JournalCloser& operator=(const JournalCloser&) = delete;
57     JournalCloser& operator=(JournalCloser&&) = delete;
58 
59     JournalCloser(sd_journal* journal) : journal{journal}
60     {}
61 
62     ~JournalCloser()
63     {
64         sd_journal_close(journal);
65     }
66 
67   private:
68     sd_journal* journal{nullptr};
69 };
70 
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(fmt::format("Could not open FFDC file {}. errno {}",
78                                     _name.string(), e));
79     }
80 }
81 
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 previous systemd journal entries as FFDC
95     auto serviceFFDC = makeJsonFFDCFile(getJournalEntries(25));
96     if (serviceFFDC && serviceFFDC->fd() != -1)
97     {
98         ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, serviceFFDC->fd());
99     }
100 
101     // Add the passed in JSON as FFDC
102     auto ffdcFile = makeJsonFFDCFile(jsonFFDC);
103     if (ffdcFile && (ffdcFile->fd() != -1))
104     {
105         ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, ffdcFile->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             fmt::format("Call to create a {} error for fan {} failed: {}",
125                         _errorName, _fanName, e.what()),
126             Logger::error);
127     }
128 }
129 
130 std::map<std::string, std::string>
131     FanError::getAdditionalData(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 
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         log<level::ERR>(
168             fmt::format("Could not save log contents in FFDC. Error msg: {}",
169                         e.what())
170                 .c_str());
171     }
172     return nullptr;
173 }
174 
175 std::unique_ptr<FFDCFile> FanError::makeJsonFFDCFile(const json& ffdcData)
176 {
177     char tmpFile[] = "/tmp/fanffdc.XXXXXX";
178     auto fd = mkstemp(tmpFile);
179     if (fd != -1)
180     {
181         auto jsonString = ffdcData.dump();
182 
183         auto rc = write(fd, jsonString.data(), jsonString.size());
184         close(fd);
185         if (rc != -1)
186         {
187             fs::path jsonFile{tmpFile};
188             return std::make_unique<FFDCFile>(jsonFile);
189         }
190         else
191         {
192             getLogger().log("Failed call to write JSON FFDC file");
193         }
194     }
195     else
196     {
197         auto e = errno;
198         getLogger().log(fmt::format("Failed called to mkstemp, errno = {}", e),
199                         Logger::error);
200     }
201     return nullptr;
202 }
203 
204 nlohmann::json FanError::getJournalEntries(int numLines) const
205 {
206     // Sleep 100ms; otherwise recent journal entries sometimes not available
207     using namespace std::chrono_literals;
208     std::this_thread::sleep_for(100ms);
209 
210     std::vector<std::string> entries;
211 
212     // Open the journal
213     sd_journal* journal;
214     int rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY);
215     if (rc < 0)
216     {
217         // Build one line string containing field values
218         entries.push_back("[Internal error: sd_journal_open(), rc=" +
219                           std::string(strerror(rc)) + "]");
220         return json(entries);
221     }
222 
223     // Create object to automatically close journal
224     JournalCloser closer{journal};
225 
226     std::string field{"SYSLOG_IDENTIFIER"};
227     std::vector<std::string> executables{"systemd"};
228 
229     entries.reserve(2 * numLines);
230 
231     for (const auto& executable : executables)
232     {
233         // Add match so we only loop over entries with specified field value
234         std::string match{field + '=' + executable};
235         rc = sd_journal_add_match(journal, match.c_str(), 0);
236         if (rc < 0)
237         {
238             // Build one line string containing field values
239             entries.push_back("[Internal error: sd_journal_add_match(), rc=" +
240                               std::string(strerror(rc)) + "]");
241 
242             break;
243         }
244 
245         int count{1};
246 
247         std::string syslogID, pid, message, timeStamp;
248 
249         // Loop through journal entries from newest to oldest
250         SD_JOURNAL_FOREACH_BACKWARDS(journal)
251         {
252             // Get relevant journal entry fields
253             timeStamp = getTimeStamp(journal);
254             syslogID = getFieldValue(journal, "SYSLOG_IDENTIFIER");
255             pid = getFieldValue(journal, "_PID");
256             message = getFieldValue(journal, "MESSAGE");
257 
258             // Build one line string containing field values
259             entries.push_back(timeStamp + " " + syslogID + "[" + pid +
260                               "]: " + message);
261 
262             // Stop after number of lines was read
263             if (count++ >= numLines)
264             {
265                 break;
266             }
267         }
268     }
269 
270     // put the journal entries in chronological order
271     std::reverse(entries.begin(), entries.end());
272 
273     return json(entries);
274 }
275 
276 std::string FanError::getTimeStamp(sd_journal* journal) const
277 {
278     // Get realtime (wallclock) timestamp of current journal entry.  The
279     // timestamp is in microseconds since the epoch.
280     uint64_t usec{0};
281     int rc = sd_journal_get_realtime_usec(journal, &usec);
282     if (rc < 0)
283     {
284         return "[Internal error: sd_journal_get_realtime_usec(), rc=" +
285                std::string(strerror(rc)) + "]";
286     }
287 
288     // Convert to number of seconds since the epoch
289     time_t secs = usec / 1000000;
290 
291     // Convert seconds to tm struct required by strftime()
292     struct tm* timeStruct = localtime(&secs);
293     if (timeStruct == nullptr)
294     {
295         return "[Internal error: localtime() returned nullptr]";
296     }
297 
298     // Convert tm struct into a date/time string
299     char timeStamp[80];
300     strftime(timeStamp, sizeof(timeStamp), "%b %d %H:%M:%S", timeStruct);
301 
302     return timeStamp;
303 }
304 
305 std::string FanError::getFieldValue(sd_journal* journal,
306                                     const std::string& field) const
307 {
308     std::string value{};
309 
310     // Get field data from current journal entry
311     const void* data{nullptr};
312     size_t length{0};
313     int rc = sd_journal_get_data(journal, field.c_str(), &data, &length);
314     if (rc < 0)
315     {
316         if (-rc == ENOENT)
317         {
318             // Current entry does not include this field; return empty value
319             return value;
320         }
321         else
322         {
323             return "[Internal error: sd_journal_get_data() rc=" +
324                    std::string(strerror(rc)) + "]";
325         }
326     }
327 
328     // Get value from field data.  Field data in format "FIELD=value".
329     std::string dataString{static_cast<const char*>(data), length};
330     std::string::size_type pos = dataString.find('=');
331     if ((pos != std::string::npos) && ((pos + 1) < dataString.size()))
332     {
333         // Value is substring after the '='
334         value = dataString.substr(pos + 1);
335     }
336 
337     return value;
338 }
339 
340 } // namespace phosphor::fan::monitor
341