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
257 std::optional<registries::MessageId> msgComponents =
258 registries::getMessageComponents(messageID);
259 if (!msgComponents)
260 {
261 return LogParseError::parseFailed;
262 }
263
264 std::optional<registries::RegistryEntryRef> registry =
265 registries::getRegistryFromPrefix(msgComponents->registryName);
266 if (!registry)
267 {
268 return LogParseError::messageIdNotInRegistry;
269 }
270
271 // Get the Message from the MessageKey and RegistryEntries
272 const registries::Message* message = registries::getMessageFromRegistry(
273 msgComponents->messageKey, registry->get().entries);
274
275 logEntryIter++;
276 if (message == nullptr)
277 {
278 BMCWEB_LOG_WARNING("Log entry not found in registry: {}", logEntry);
279 return LogParseError::messageIdNotInRegistry;
280 }
281
282 const unsigned int& versionMajor = registry->get().header.versionMajor;
283 const unsigned int& versionMinor = registry->get().header.versionMinor;
284
285 std::vector<std::string_view> messageArgs(logEntryIter,
286 logEntryFields.end());
287 messageArgs.resize(message->numberOfArgs);
288
289 std::string msg =
290 redfish::registries::fillMessageArgs(messageArgs, message->message);
291 if (msg.empty())
292 {
293 return LogParseError::parseFailed;
294 }
295
296 // Get the Created time from the timestamp. The log timestamp is in RFC3339
297 // format which matches the Redfish format except for the
298 // fractional seconds between the '.' and the '+', so just remove them.
299 std::size_t dot = timestamp.find_first_of('.');
300 std::size_t plus = timestamp.find_first_of('+');
301 if (dot != std::string::npos && plus != std::string::npos)
302 {
303 timestamp.erase(dot, plus - dot);
304 }
305
306 // Fill in the log entry with the gathered data
307 logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
308 logEntryJson["@odata.id"] =
309 boost::urls::format("/redfish/v1/{}/{}/LogServices/EventLog/Entries/{}",
310 parentStr, childId, logEntryID);
311 logEntryJson["Name"] =
312 std::format("{} Event Log Entry", logEntryDescriptor);
313 logEntryJson["Id"] = logEntryID;
314 logEntryJson["Message"] = std::move(msg);
315 logEntryJson["MessageId"] =
316 std::format("{}.{}.{}.{}", msgComponents->registryName, versionMajor,
317 versionMinor, msgComponents->messageKey);
318 logEntryJson["MessageArgs"] = messageArgs;
319 logEntryJson["EntryType"] = "Event";
320 logEntryJson["Severity"] = message->messageSeverity;
321 logEntryJson["Created"] = std::move(timestamp);
322 return LogParseError::success;
323 }
324
handleSystemsAndManagersLogServiceEventLogLogEntryCollection(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,query_param::Query & delegatedQuery,LogServiceParent parent)325 inline void handleSystemsAndManagersLogServiceEventLogLogEntryCollection(
326 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
327 query_param::Query& delegatedQuery, LogServiceParent parent)
328 {
329 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop);
330 size_t skip = delegatedQuery.skip.value_or(0);
331
332 const std::string parentStr = logServiceParentToString(parent);
333 const std::string_view childId = getChildIdFromParent(parent);
334 const std::string logEntryDescriptor = getLogEntryDescriptor(parent);
335
336 if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty())
337 {
338 messages::internalError(asyncResp->res);
339 return;
340 }
341
342 // Collections don't include the static data added by SubRoute
343 // because it has a duplicate entry for members
344 asyncResp->res.jsonValue["@odata.type"] =
345 "#LogEntryCollection.LogEntryCollection";
346 asyncResp->res.jsonValue["@odata.id"] = std::format(
347 "/redfish/v1/{}/{}/LogServices/EventLog/Entries", parentStr, childId);
348 asyncResp->res.jsonValue["Name"] =
349 std::format("{} Event Log Entries", logEntryDescriptor);
350 asyncResp->res.jsonValue["Description"] =
351 std::format("Collection of {} Event Log Entries", logEntryDescriptor);
352
353 nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"];
354 logEntryArray = nlohmann::json::array();
355 // Go through the log files and create a unique ID for each
356 // entry
357 std::vector<std::filesystem::path> redfishLogFiles;
358 getRedfishLogFiles(redfishLogFiles);
359 uint64_t entryCount = 0;
360 std::string logEntry;
361
362 // Oldest logs are in the last file, so start there and loop
363 // backwards
364 for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
365 {
366 std::ifstream logStream(*it);
367 if (!logStream.is_open())
368 {
369 continue;
370 }
371
372 // Reset the unique ID on the first entry
373 bool firstEntry = true;
374 while (std::getline(logStream, logEntry))
375 {
376 std::string idStr;
377 if (!getUniqueEntryID(logEntry, idStr, firstEntry))
378 {
379 continue;
380 }
381 firstEntry = false;
382
383 nlohmann::json::object_t bmcLogEntry;
384 LogParseError status =
385 fillEventLogEntryJson(idStr, logEntry, bmcLogEntry, parentStr,
386 childId, logEntryDescriptor);
387 if (status == LogParseError::messageIdNotInRegistry)
388 {
389 continue;
390 }
391 if (status != LogParseError::success)
392 {
393 messages::internalError(asyncResp->res);
394 return;
395 }
396
397 entryCount++;
398 // Handle paging using skip (number of entries to skip from the
399 // start) and top (number of entries to display)
400 if (entryCount <= skip || entryCount > skip + top)
401 {
402 continue;
403 }
404
405 logEntryArray.emplace_back(std::move(bmcLogEntry));
406 }
407 }
408 asyncResp->res.jsonValue["Members@odata.count"] = entryCount;
409 if (skip + top < entryCount)
410 {
411 asyncResp->res.jsonValue["Members@odata.nextLink"] =
412 boost::urls::format(
413 "/redfish/v1/{}/{}/LogServices/EventLog/Entries?$skip={}",
414 parentStr, childId, std::to_string(skip + top));
415 }
416 }
417
handleSystemsAndManagersLogServiceEventLogEntriesGet(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & param,LogServiceParent parent)418 inline void handleSystemsAndManagersLogServiceEventLogEntriesGet(
419 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
420 const std::string& param, LogServiceParent parent)
421 {
422 const std::string& targetID = param;
423
424 const std::string parentStr = logServiceParentToString(parent);
425 const std::string_view childId = getChildIdFromParent(parent);
426 const std::string logEntryDescriptor = getLogEntryDescriptor(parent);
427
428 if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty())
429 {
430 messages::internalError(asyncResp->res);
431 return;
432 }
433
434 // Go through the log files and check the unique ID for each
435 // entry to find the target entry
436 std::vector<std::filesystem::path> redfishLogFiles;
437 getRedfishLogFiles(redfishLogFiles);
438 std::string logEntry;
439
440 // Oldest logs are in the last file, so start there and loop
441 // backwards
442 for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++)
443 {
444 std::ifstream logStream(*it);
445 if (!logStream.is_open())
446 {
447 continue;
448 }
449
450 // Reset the unique ID on the first entry
451 bool firstEntry = true;
452 while (std::getline(logStream, logEntry))
453 {
454 std::string idStr;
455 if (!getUniqueEntryID(logEntry, idStr, firstEntry))
456 {
457 continue;
458 }
459 firstEntry = false;
460
461 if (idStr == targetID)
462 {
463 nlohmann::json::object_t bmcLogEntry;
464 LogParseError status = fillEventLogEntryJson(
465 idStr, logEntry, bmcLogEntry, parentStr, childId,
466 logEntryDescriptor);
467 if (status != LogParseError::success)
468 {
469 messages::internalError(asyncResp->res);
470 return;
471 }
472 asyncResp->res.jsonValue.update(bmcLogEntry);
473 return;
474 }
475 }
476 }
477 // Requested ID was not found
478 messages::resourceNotFound(asyncResp->res, "LogEntry", targetID);
479 }
480
handleSystemsAndManagersLogServicesEventLogActionsClearPost(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)481 inline void handleSystemsAndManagersLogServicesEventLogActionsClearPost(
482 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
483 {
484 // Clear the EventLog by deleting the log files
485 std::vector<std::filesystem::path> redfishLogFiles;
486 if (getRedfishLogFiles(redfishLogFiles))
487 {
488 for (const std::filesystem::path& file : redfishLogFiles)
489 {
490 std::error_code ec;
491 std::filesystem::remove(file, ec);
492 }
493 }
494
495 // Reload rsyslog so it knows to start new log files
496 dbus::utility::async_method_call(
497 asyncResp,
498 [asyncResp](const boost::system::error_code& ec) {
499 if (ec)
500 {
501 BMCWEB_LOG_ERROR("Failed to reload rsyslog: {}", ec);
502 messages::internalError(asyncResp->res);
503 return;
504 }
505
506 messages::success(asyncResp->res);
507 },
508 "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
509 "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service",
510 "replace");
511 }
512 } // namespace eventlog_utils
513 } // namespace redfish
514