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 "http_response.hpp" 10 #include "logging.hpp" 11 #include "registries.hpp" 12 #include "str_utility.hpp" 13 #include "utils/query_param.hpp" 14 15 #include <boost/beast/http/field.hpp> 16 #include <boost/beast/http/status.hpp> 17 #include <boost/beast/http/verb.hpp> 18 #include <boost/system/linux_error.hpp> 19 #include <boost/url/format.hpp> 20 #include <boost/url/url.hpp> 21 #include <sdbusplus/message.hpp> 22 #include <sdbusplus/message/native_types.hpp> 23 #include <sdbusplus/unpack_properties.hpp> 24 25 #include <algorithm> 26 #include <cstddef> 27 #include <cstdint> 28 #include <cstdio> 29 #include <ctime> 30 #include <fstream> 31 #include <iomanip> 32 #include <sstream> 33 #include <string> 34 #include <utility> 35 36 namespace redfish 37 { 38 namespace eventlog_utils 39 { 40 41 constexpr const char* rfSystemsStr = "Systems"; 42 constexpr const char* rfManagersStr = "Managers"; 43 44 enum class LogServiceParent 45 { 46 Systems, 47 Managers 48 }; 49 50 inline std::string logServiceParentToString(LogServiceParent parent) 51 { 52 std::string parentStr; 53 switch (parent) 54 { 55 case LogServiceParent::Managers: 56 parentStr = rfManagersStr; 57 break; 58 case LogServiceParent::Systems: 59 parentStr = rfSystemsStr; 60 break; 61 default: 62 BMCWEB_LOG_ERROR("Unable to stringify bmcweb eventlog location"); 63 break; 64 } 65 return parentStr; 66 } 67 68 inline std::string_view getChildIdFromParent(LogServiceParent parent) 69 { 70 std::string_view childId; 71 72 switch (parent) 73 { 74 case LogServiceParent::Managers: 75 childId = BMCWEB_REDFISH_MANAGER_URI_NAME; 76 break; 77 case LogServiceParent::Systems: 78 childId = BMCWEB_REDFISH_SYSTEM_URI_NAME; 79 break; 80 default: 81 BMCWEB_LOG_ERROR( 82 "Unable to stringify bmcweb eventlog location childId"); 83 break; 84 } 85 return childId; 86 } 87 88 inline std::string getLogEntryDescriptor(LogServiceParent parent) 89 { 90 std::string descriptor; 91 switch (parent) 92 { 93 case LogServiceParent::Managers: 94 descriptor = "Manager"; 95 break; 96 case LogServiceParent::Systems: 97 descriptor = "System"; 98 break; 99 default: 100 BMCWEB_LOG_ERROR("Unable to get Log Entry descriptor"); 101 break; 102 } 103 return descriptor; 104 } 105 106 /* 107 * Journal EventLog utilities 108 * */ 109 110 inline bool getRedfishLogFiles( 111 std::vector<std::filesystem::path>& redfishLogFiles) 112 { 113 static const std::filesystem::path redfishLogDir = "/var/log"; 114 static const std::string redfishLogFilename = "redfish"; 115 116 // Loop through the directory looking for redfish log files 117 for (const std::filesystem::directory_entry& dirEnt : 118 std::filesystem::directory_iterator(redfishLogDir)) 119 { 120 // If we find a redfish log file, save the path 121 std::string filename = dirEnt.path().filename(); 122 if (filename.starts_with(redfishLogFilename)) 123 { 124 redfishLogFiles.emplace_back(redfishLogDir / filename); 125 } 126 } 127 // As the log files rotate, they are appended with a ".#" that is higher for 128 // the older logs. Since we don't expect more than 10 log files, we 129 // can just sort the list to get them in order from newest to oldest 130 std::ranges::sort(redfishLogFiles); 131 132 return !redfishLogFiles.empty(); 133 } 134 135 inline bool getUniqueEntryID(const std::string& logEntry, std::string& entryID, 136 const bool firstEntry = true) 137 { 138 static time_t prevTs = 0; 139 static int index = 0; 140 if (firstEntry) 141 { 142 prevTs = 0; 143 } 144 145 // Get the entry timestamp 146 std::time_t curTs = 0; 147 std::tm timeStruct = {}; 148 std::istringstream entryStream(logEntry); 149 if (entryStream >> std::get_time(&timeStruct, "%Y-%m-%dT%H:%M:%S")) 150 { 151 curTs = std::mktime(&timeStruct); 152 } 153 // If the timestamp isn't unique, increment the index 154 if (curTs == prevTs) 155 { 156 index++; 157 } 158 else 159 { 160 // Otherwise, reset it 161 index = 0; 162 } 163 // Save the timestamp 164 prevTs = curTs; 165 166 entryID = std::to_string(curTs); 167 if (index > 0) 168 { 169 entryID += "_" + std::to_string(index); 170 } 171 return true; 172 } 173 174 enum class LogParseError 175 { 176 success, 177 parseFailed, 178 messageIdNotInRegistry, 179 }; 180 181 static LogParseError fillEventLogEntryJson( 182 const std::string& logEntryID, const std::string& logEntry, 183 nlohmann::json::object_t& logEntryJson, const std::string& parentStr, 184 const std::string_view childId, const std::string& logEntryDescriptor) 185 { 186 // The redfish log format is "<Timestamp> <MessageId>,<MessageArgs>" 187 // First get the Timestamp 188 size_t space = logEntry.find_first_of(' '); 189 if (space == std::string::npos) 190 { 191 return LogParseError::parseFailed; 192 } 193 std::string timestamp = logEntry.substr(0, space); 194 // Then get the log contents 195 size_t entryStart = logEntry.find_first_not_of(' ', space); 196 if (entryStart == std::string::npos) 197 { 198 return LogParseError::parseFailed; 199 } 200 std::string_view entry(logEntry); 201 entry.remove_prefix(entryStart); 202 // Use split to separate the entry into its fields 203 std::vector<std::string> logEntryFields; 204 bmcweb::split(logEntryFields, entry, ','); 205 // We need at least a MessageId to be valid 206 auto logEntryIter = logEntryFields.begin(); 207 if (logEntryIter == logEntryFields.end()) 208 { 209 return LogParseError::parseFailed; 210 } 211 std::string& messageID = *logEntryIter; 212 // Get the Message from the MessageRegistry 213 const registries::Message* message = registries::getMessage(messageID); 214 215 logEntryIter++; 216 if (message == nullptr) 217 { 218 BMCWEB_LOG_WARNING("Log entry not found in registry: {}", logEntry); 219 return LogParseError::messageIdNotInRegistry; 220 } 221 222 std::vector<std::string_view> messageArgs(logEntryIter, 223 logEntryFields.end()); 224 messageArgs.resize(message->numberOfArgs); 225 226 std::string msg = 227 redfish::registries::fillMessageArgs(messageArgs, message->message); 228 if (msg.empty()) 229 { 230 return LogParseError::parseFailed; 231 } 232 233 // Get the Created time from the timestamp. The log timestamp is in RFC3339 234 // format which matches the Redfish format except for the fractional seconds 235 // between the '.' and the '+', so just remove them. 236 std::size_t dot = timestamp.find_first_of('.'); 237 std::size_t plus = timestamp.find_first_of('+'); 238 if (dot != std::string::npos && plus != std::string::npos) 239 { 240 timestamp.erase(dot, plus - dot); 241 } 242 243 // Fill in the log entry with the gathered data 244 logEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 245 logEntryJson["@odata.id"] = 246 boost::urls::format("/redfish/v1/{}/{}/LogServices/EventLog/Entries/{}", 247 parentStr, childId, logEntryID); 248 logEntryJson["Name"] = 249 std::format("{} Event Log Entry", logEntryDescriptor); 250 logEntryJson["Id"] = logEntryID; 251 logEntryJson["Message"] = std::move(msg); 252 logEntryJson["MessageId"] = std::move(messageID); 253 logEntryJson["MessageArgs"] = messageArgs; 254 logEntryJson["EntryType"] = "Event"; 255 logEntryJson["Severity"] = message->messageSeverity; 256 logEntryJson["Created"] = std::move(timestamp); 257 return LogParseError::success; 258 } 259 260 inline void handleRequestSystemsLogServiceEventLogLogEntryCollection( 261 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 262 query_param::Query& delegatedQuery, LogServiceParent parent) 263 { 264 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 265 size_t skip = delegatedQuery.skip.value_or(0); 266 267 const std::string parentStr = logServiceParentToString(parent); 268 const std::string_view childId = getChildIdFromParent(parent); 269 const std::string logEntryDescriptor = getLogEntryDescriptor(parent); 270 271 if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty()) 272 { 273 messages::internalError(asyncResp->res); 274 return; 275 } 276 277 // Collections don't include the static data added by SubRoute 278 // because it has a duplicate entry for members 279 asyncResp->res.jsonValue["@odata.type"] = 280 "#LogEntryCollection.LogEntryCollection"; 281 asyncResp->res.jsonValue["@odata.id"] = std::format( 282 "/redfish/v1/{}/{}/LogServices/EventLog/Entries", parentStr, childId); 283 asyncResp->res.jsonValue["Name"] = 284 std::format("{} Event Log Entries", logEntryDescriptor); 285 asyncResp->res.jsonValue["Description"] = 286 std::format("Collection of {} Event Log Entries", logEntryDescriptor); 287 288 nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; 289 logEntryArray = nlohmann::json::array(); 290 // Go through the log files and create a unique ID for each 291 // entry 292 std::vector<std::filesystem::path> redfishLogFiles; 293 getRedfishLogFiles(redfishLogFiles); 294 uint64_t entryCount = 0; 295 std::string logEntry; 296 297 // Oldest logs are in the last file, so start there and loop 298 // backwards 299 for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++) 300 { 301 std::ifstream logStream(*it); 302 if (!logStream.is_open()) 303 { 304 continue; 305 } 306 307 // Reset the unique ID on the first entry 308 bool firstEntry = true; 309 while (std::getline(logStream, logEntry)) 310 { 311 std::string idStr; 312 if (!getUniqueEntryID(logEntry, idStr, firstEntry)) 313 { 314 continue; 315 } 316 firstEntry = false; 317 318 nlohmann::json::object_t bmcLogEntry; 319 LogParseError status = 320 fillEventLogEntryJson(idStr, logEntry, bmcLogEntry, parentStr, 321 childId, logEntryDescriptor); 322 if (status == LogParseError::messageIdNotInRegistry) 323 { 324 continue; 325 } 326 if (status != LogParseError::success) 327 { 328 messages::internalError(asyncResp->res); 329 return; 330 } 331 332 entryCount++; 333 // Handle paging using skip (number of entries to skip from the 334 // start) and top (number of entries to display) 335 if (entryCount <= skip || entryCount > skip + top) 336 { 337 continue; 338 } 339 340 logEntryArray.emplace_back(std::move(bmcLogEntry)); 341 } 342 } 343 asyncResp->res.jsonValue["Members@odata.count"] = entryCount; 344 if (skip + top < entryCount) 345 { 346 asyncResp->res.jsonValue["Members@odata.nextLink"] = 347 boost::urls::format( 348 "/redfish/v1/{}/{}/LogServices/EventLog/Entries?$skip={}", 349 parentStr, childId, std::to_string(skip + top)); 350 } 351 } 352 353 inline void handleRequestSystemsLogServiceEventLogEntriesGet( 354 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 355 const std::string& param, LogServiceParent parent) 356 { 357 const std::string& targetID = param; 358 359 const std::string parentStr = logServiceParentToString(parent); 360 const std::string_view childId = getChildIdFromParent(parent); 361 const std::string logEntryDescriptor = getLogEntryDescriptor(parent); 362 363 if (parentStr.empty() || childId.empty() || logEntryDescriptor.empty()) 364 { 365 messages::internalError(asyncResp->res); 366 return; 367 } 368 369 // Go through the log files and check the unique ID for each 370 // entry to find the target entry 371 std::vector<std::filesystem::path> redfishLogFiles; 372 getRedfishLogFiles(redfishLogFiles); 373 std::string logEntry; 374 375 // Oldest logs are in the last file, so start there and loop 376 // backwards 377 for (auto it = redfishLogFiles.rbegin(); it < redfishLogFiles.rend(); it++) 378 { 379 std::ifstream logStream(*it); 380 if (!logStream.is_open()) 381 { 382 continue; 383 } 384 385 // Reset the unique ID on the first entry 386 bool firstEntry = true; 387 while (std::getline(logStream, logEntry)) 388 { 389 std::string idStr; 390 if (!getUniqueEntryID(logEntry, idStr, firstEntry)) 391 { 392 continue; 393 } 394 firstEntry = false; 395 396 if (idStr == targetID) 397 { 398 nlohmann::json::object_t bmcLogEntry; 399 LogParseError status = fillEventLogEntryJson( 400 idStr, logEntry, bmcLogEntry, parentStr, childId, 401 logEntryDescriptor); 402 if (status != LogParseError::success) 403 { 404 messages::internalError(asyncResp->res); 405 return; 406 } 407 asyncResp->res.jsonValue.update(bmcLogEntry); 408 return; 409 } 410 } 411 } 412 // Requested ID was not found 413 messages::resourceNotFound(asyncResp->res, "LogEntry", targetID); 414 } 415 416 inline void handleRequestSystemsLogServicesEventLogActionsClearPost( 417 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 418 { 419 // Clear the EventLog by deleting the log files 420 std::vector<std::filesystem::path> redfishLogFiles; 421 if (getRedfishLogFiles(redfishLogFiles)) 422 { 423 for (const std::filesystem::path& file : redfishLogFiles) 424 { 425 std::error_code ec; 426 std::filesystem::remove(file, ec); 427 } 428 } 429 430 // Reload rsyslog so it knows to start new log files 431 dbus::utility::async_method_call( 432 asyncResp, 433 [asyncResp](const boost::system::error_code& ec) { 434 if (ec) 435 { 436 BMCWEB_LOG_ERROR("Failed to reload rsyslog: {}", ec); 437 messages::internalError(asyncResp->res); 438 return; 439 } 440 441 messages::success(asyncResp->res); 442 }, 443 "org.freedesktop.systemd1", "/org/freedesktop/systemd1", 444 "org.freedesktop.systemd1.Manager", "ReloadUnit", "rsyslog.service", 445 "replace"); 446 } 447 } // namespace eventlog_utils 448 } // namespace redfish 449