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