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