xref: /openbmc/bmcweb/redfish-core/lib/systems_logservices_journal_eventlog.hpp (revision 75dac00e3c7046b145a3e8823511909a350bc473)
1 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
3 #pragma once
4 
5 #include "bmcweb_config.h"
6 
7 #include "app.hpp"
8 #include "async_resp.hpp"
9 #include "dbus_utility.hpp"
10 #include "error_messages.hpp"
11 #include "http_request.hpp"
12 #include "http_response.hpp"
13 #include "logging.hpp"
14 #include "query.hpp"
15 #include "registries.hpp"
16 #include "registries/privilege_registry.hpp"
17 #include "str_utility.hpp"
18 #include "utils/query_param.hpp"
19 
20 #include <asm-generic/errno.h>
21 #include <systemd/sd-bus.h>
22 #include <unistd.h>
23 
24 #include <boost/beast/http/field.hpp>
25 #include <boost/beast/http/status.hpp>
26 #include <boost/beast/http/verb.hpp>
27 #include <boost/system/linux_error.hpp>
28 #include <boost/url/format.hpp>
29 #include <boost/url/url.hpp>
30 #include <sdbusplus/message.hpp>
31 #include <sdbusplus/message/native_types.hpp>
32 #include <sdbusplus/unpack_properties.hpp>
33 
34 #include <algorithm>
35 #include <cstddef>
36 #include <cstdint>
37 #include <cstdio>
38 #include <ctime>
39 #include <filesystem>
40 #include <format>
41 #include <fstream>
42 #include <functional>
43 #include <iomanip>
44 #include <iterator>
45 #include <memory>
46 #include <optional>
47 #include <span>
48 #include <sstream>
49 #include <string>
50 #include <string_view>
51 #include <system_error>
52 #include <utility>
53 #include <vector>
54 
55 namespace redfish
56 {
57 
getRedfishLogFiles(std::vector<std::filesystem::path> & redfishLogFiles)58 inline bool getRedfishLogFiles(
59     std::vector<std::filesystem::path>& redfishLogFiles)
60 {
61     static const std::filesystem::path redfishLogDir = "/var/log";
62     static const std::string redfishLogFilename = "redfish";
63 
64     // Loop through the directory looking for redfish log files
65     for (const std::filesystem::directory_entry& dirEnt :
66          std::filesystem::directory_iterator(redfishLogDir))
67     {
68         // If we find a redfish log file, save the path
69         std::string filename = dirEnt.path().filename();
70         if (filename.starts_with(redfishLogFilename))
71         {
72             redfishLogFiles.emplace_back(redfishLogDir / filename);
73         }
74     }
75     // As the log files rotate, they are appended with a ".#" that is higher for
76     // the older logs. Since we don't expect more than 10 log files, we
77     // can just sort the list to get them in order from newest to oldest
78     std::ranges::sort(redfishLogFiles);
79 
80     return !redfishLogFiles.empty();
81 }
82 
getUniqueEntryID(const std::string & logEntry,std::string & entryID,const bool firstEntry=true)83 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID,
84                              const bool firstEntry = true)
85 {
86     static time_t prevTs = 0;
87     static int index = 0;
88     if (firstEntry)
89     {
90         prevTs = 0;
91     }
92 
93     // Get the entry timestamp
94     std::time_t curTs = 0;
95     std::tm timeStruct = {};
96     std::istringstream entryStream(logEntry);
97     if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S"))
98     {
99         curTs = std::mktime(&timeStruct);
100     }
101     // If the timestamp isn't unique, increment the index
102     if (curTs == prevTs)
103     {
104         index++;
105     }
106     else
107     {
108         // Otherwise, reset it
109         index = 0;
110     }
111     // Save the timestamp
112     prevTs = curTs;
113 
114     entryID = std::to_string(curTs);
115     if (index > 0)
116     {
117         entryID += "_" + std::to_string(index);
118     }
119     return true;
120 }
121 
122 enum class LogParseError
123 {
124     success,
125     parseFailed,
126     messageIdNotInRegistry,
127 };
128 
fillEventLogEntryJson(const std::string & logEntryID,const std::string & logEntry,nlohmann::json::object_t & logEntryJson)129 static LogParseError fillEventLogEntryJson(
130     const std::string& logEntryID, const std::string& logEntry,
131     nlohmann::json::object_t& logEntryJson)
132 {
133     // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>"
134     // First get the Timestamp
135     size_t space = logEntry.find_first_of(' ');
136     if (space == std::string::npos)
137     {
138         return LogParseError::parseFailed;
139     }
140     std::string timestamp = logEntry.substr(0, space);
141     // Then get the log contents
142     size_t entryStart = logEntry.find_first_not_of(' ', space);
143     if (entryStart == std::string::npos)
144     {
145         return LogParseError::parseFailed;
146     }
147     std::string_view entry(logEntry);
148     entry.remove_prefix(entryStart);
149     // Use split to separate the entry into its fields
150     std::vector<std::string> logEntryFields;
151     bmcweb::split(logEntryFields, entry, ',');
152     // We need at least a MessageId to be valid
153     auto logEntryIter = logEntryFields.begin();
154     if (logEntryIter == logEntryFields.end())
155     {
156         return LogParseError::parseFailed;
157     }
158     std::string& messageID = *logEntryIter;
159     // Get the Message from the MessageRegistry
160     const registries::Message* message = registries::getMessage(messageID);
161 
162     logEntryIter++;
163     if (message == nullptr)
164     {
165         BMCWEB_LOG_WARNING("Log entry not found in registry: {}", logEntry);
166         return LogParseError::messageIdNotInRegistry;
167     }
168 
169     std::vector<std::string_view> messageArgs(logEntryIter,
170                                               logEntryFields.end());
171     messageArgs.resize(message->numberOfArgs);
172 
173     std::string msg =
174         redfish::registries::fillMessageArgs(messageArgs, message->message);
175     if (msg.empty())
176     {
177         return LogParseError::parseFailed;
178     }
179 
180     // Get the Created time from the timestamp. The log timestamp is in RFC3339
181     // format which matches the Redfish format except for the fractional seconds
182     // between the '.' and the '+', so just remove them.
183     std::size_t dot = timestamp.find_first_of('.');
184     std::size_t plus = timestamp.find_first_of('+');
185     if (dot != std::string::npos && plus != std::string::npos)
186     {
187         timestamp.erase(dot, plus - dot);
188     }
189 
190     // Fill in the log entry with the gathered data
191     logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
192     logEntryJson["@odata.id"] = boost::urls::format(
193         "/redfish/v1/Systems/{}/LogServices/EventLog/Entries/{}",
194         BMCWEB_REDFISH_SYSTEM_URI_NAME, logEntryID);
195     logEntryJson["Name"] = "System Event Log Entry";
196     logEntryJson["Id"] = logEntryID;
197     logEntryJson["Message"] = std::move(msg);
198     logEntryJson["MessageId"] = std::move(messageID);
199     logEntryJson["MessageArgs"] = messageArgs;
200     logEntryJson["EntryType"] = "Event";
201     logEntryJson["Severity"] = message->messageSeverity;
202     logEntryJson["Created"] = std::move(timestamp);
203     return LogParseError::success;
204 }
205 
handleSystemsLogServiceEventLogLogEntryCollection(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & systemName)206 inline void handleSystemsLogServiceEventLogLogEntryCollection(
207     App& app, const crow::Request& req,
208     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
209     const std::string& systemName)
210 {
211     query_param::QueryCapabilities capabilities = {
212         .canDelegateTop = true,
213         .canDelegateSkip = true,
214     };
215     query_param::Query delegatedQuery;
216     if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp,
217                                                   delegatedQuery, capabilities))
218     {
219         return;
220     }
221     if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM)
222     {
223         // Option currently returns no systems.  TBD
224         messages::resourceNotFound(asyncResp->res, "ComputerSystem",
225                                    systemName);
226         return;
227     }
228     if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
229     {
230         messages::resourceNotFound(asyncResp->res, "ComputerSystem",
231                                    systemName);
232         return;
233     }
234 
235     size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
236     size_t skip = delegatedQuery.skip.value_or(0);
237 
238     // Collections don't include the static data added by SubRoute
239     // because it has a duplicate entry for members
240     asyncResp->res.jsonValue["@odata.type"] =
241         "#LogEntryCollection.LogEntryCollection";
242     asyncResp->res.jsonValue["@odata.id"] =
243         std::format("/redfish/v1/Systems/{}/LogServices/EventLog/Entries",
244                     BMCWEB_REDFISH_SYSTEM_URI_NAME);
245     asyncResp->res.jsonValue["Name"] = "System Event Log Entries";
246     asyncResp->res.jsonValue["Description"] =
247         "Collection of System Event Log Entries";
248 
249     nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"];
250     logEntryArray = nlohmann::json::array();
251     // Go through the log files and create a unique ID for each
252     // entry
253     std::vector<std::filesystem::path> redfishLogFiles;
254     getRedfishLogFiles(redfishLogFiles);
255     uint64_t entryCount = 0;
256     std::string logEntry;
257 
258     // Oldest logs are in the last file, so start there and loop
259     // backwards
260     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
261     {
262         std::ifstream logStream(*it);
263         if (!logStream.is_open())
264         {
265             continue;
266         }
267 
268         // Reset the unique ID on the first entry
269         bool firstEntry = true;
270         while (std::getline(logStream, logEntry))
271         {
272             std::string idStr;
273             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
274             {
275                 continue;
276             }
277             firstEntry = false;
278 
279             nlohmann::json::object_t bmcLogEntry;
280             LogParseError status =
281                 fillEventLogEntryJson(idStr, logEntry, bmcLogEntry);
282             if (status == LogParseError::messageIdNotInRegistry)
283             {
284                 continue;
285             }
286             if (status != LogParseError::success)
287             {
288                 messages::internalError(asyncResp->res);
289                 return;
290             }
291 
292             entryCount++;
293             // Handle paging using skip (number of entries to skip from the
294             // start) and top (number of entries to display)
295             if (entryCount <= skip || entryCount > skip + top)
296             {
297                 continue;
298             }
299 
300             logEntryArray.emplace_back(std::move(bmcLogEntry));
301         }
302     }
303     asyncResp->res.jsonValue["Members@odata.count"] = entryCount;
304     if (skip + top < entryCount)
305     {
306         asyncResp->res.jsonValue["Members@odata.nextLink"] =
307             boost::urls::format(
308                 "/redfish/v1/Systems/{}/LogServices/EventLog/Entries?$skip={}",
309                 BMCWEB_REDFISH_SYSTEM_URI_NAME, std::to_string(skip + top));
310     }
311 }
312 
handleSystemsLogServiceEventLogEntriesGet(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & systemName,const std::string & param)313 inline void handleSystemsLogServiceEventLogEntriesGet(
314     App& app, const crow::Request& req,
315     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
316     const std::string& systemName, const std::string& param)
317 {
318     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
319     {
320         return;
321     }
322     if constexpr (BMCWEB_EXPERIMENTAL_REDFISH_MULTI_COMPUTER_SYSTEM)
323     {
324         // Option currently returns no systems.  TBD
325         messages::resourceNotFound(asyncResp->res, "ComputerSystem",
326                                    systemName);
327         return;
328     }
329 
330     if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
331     {
332         messages::resourceNotFound(asyncResp->res, "ComputerSystem",
333                                    systemName);
334         return;
335     }
336 
337     const std::string& targetID = param;
338 
339     // Go through the log files and check the unique ID for each
340     // entry to find the target entry
341     std::vector<std::filesystem::path> redfishLogFiles;
342     getRedfishLogFiles(redfishLogFiles);
343     std::string logEntry;
344 
345     // Oldest logs are in the last file, so start there and loop
346     // backwards
347     for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
348     {
349         std::ifstream logStream(*it);
350         if (!logStream.is_open())
351         {
352             continue;
353         }
354 
355         // Reset the unique ID on the first entry
356         bool firstEntry = true;
357         while (std::getline(logStream, logEntry))
358         {
359             std::string idStr;
360             if (!getUniqueEntryID(logEntry, idStr, firstEntry))
361             {
362                 continue;
363             }
364             firstEntry = false;
365 
366             if (idStr == targetID)
367             {
368                 nlohmann::json::object_t bmcLogEntry;
369                 LogParseError status =
370                     fillEventLogEntryJson(idStr, logEntry, bmcLogEntry);
371                 if (status != LogParseError::success)
372                 {
373                     messages::internalError(asyncResp->res);
374                     return;
375                 }
376                 asyncResp->res.jsonValue.update(bmcLogEntry);
377                 return;
378             }
379         }
380     }
381     // Requested ID was not found
382     messages::resourceNotFound(asyncResp->res, "LogEntry", targetID);
383 }
384 
handleSystemsLogServicesEventLogActionsClearPost(App & app,const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & systemName)385 inline void handleSystemsLogServicesEventLogActionsClearPost(
386     App& app, const crow::Request& req,
387     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
388     const std::string& systemName)
389 {
390     if (!redfish::setUpRedfishRoute(app, req, asyncResp))
391     {
392         return;
393     }
394     if (systemName != BMCWEB_REDFISH_SYSTEM_URI_NAME)
395     {
396         messages::resourceNotFound(asyncResp->res, "ComputerSystem",
397                                    systemName);
398         return;
399     }
400 
401     // Clear the EventLog by deleting the log files
402     std::vector<std::filesystem::path> redfishLogFiles;
403     if (getRedfishLogFiles(redfishLogFiles))
404     {
405         for (const std::filesystem::path& file : redfishLogFiles)
406         {
407             std::error_code ec;
408             std::filesystem::remove(file, ec);
409         }
410     }
411 
412     // Reload rsyslog so it knows to start new log files
413     dbus::utility::async_method_call(
414         asyncResp,
415         [asyncResp](const boost::system::error_code& ec) {
416             if (ec)
417             {
418                 BMCWEB_LOG_ERROR("Failed to reload rsyslog: {}", ec);
419                 messages::internalError(asyncResp->res);
420                 return;
421             }
422 
423             messages::success(asyncResp->res);
424         },
425         "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
426         "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service",
427         "replace");
428 }
429 
requestRoutesJournalEventLogEntryCollection(App & app)430 inline void requestRoutesJournalEventLogEntryCollection(App& app)
431 {
432     BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/EventLog/Entries/")
433         .privileges(redfish::privileges::getLogEntryCollection)
434         .methods(boost::beast::http::verb::get)(std::bind_front(
435             handleSystemsLogServiceEventLogLogEntryCollection, std::ref(app)));
436 }
437 
requestRoutesJournalEventLogEntry(App & app)438 inline void requestRoutesJournalEventLogEntry(App& app)
439 {
440     BMCWEB_ROUTE(
441         app, "/redfish/v1/Systems/<str>/LogServices/EventLog/Entries/<str>/")
442         .privileges(redfish::privileges::getLogEntry)
443         .methods(boost::beast::http::verb::get)(std::bind_front(
444             handleSystemsLogServiceEventLogEntriesGet, std::ref(app)));
445 }
446 
requestRoutesJournalEventLogClear(App & app)447 inline void requestRoutesJournalEventLogClear(App& app)
448 {
449     BMCWEB_ROUTE(
450         app,
451         "/redfish/v1/Systems/<str>/LogServices/EventLog/Actions/LogService.ClearLog/")
452         .privileges(redfish::privileges::
453                         postLogServiceSubOverComputerSystemLogServiceCollection)
454         .methods(boost::beast::http::verb::post)(std::bind_front(
455             handleSystemsLogServicesEventLogActionsClearPost, std::ref(app)));
456 }
457 } // namespace redfish
458