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