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 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 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 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 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 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 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 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 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 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