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 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 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 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 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 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 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 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 304 inline void handleRequestSystemsLogServiceEventLogLogEntryCollection( 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 397 inline void handleRequestSystemsLogServiceEventLogEntriesGet( 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 460 inline void handleRequestSystemsLogServicesEventLogActionsClearPost( 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