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