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