1 #pragma once 2 3 #include "app.hpp" 4 #include "error_messages.hpp" 5 #include "generated/enums/log_entry.hpp" 6 #include "query.hpp" 7 #include "registries/base_message_registry.hpp" 8 #include "registries/privilege_registry.hpp" 9 #include "utils/time_utils.hpp" 10 11 #include <systemd/sd-journal.h> 12 13 #include <boost/beast/http/verb.hpp> 14 15 #include <array> 16 #include <memory> 17 #include <string> 18 #include <string_view> 19 20 namespace redfish 21 { 22 // Entry is formed like "BootID_timestamp" or "BootID_timestamp_index" 23 inline bool 24 getTimestampFromID(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 25 std::string_view entryIDStrView, sd_id128_t& bootID, 26 uint64_t& timestamp, uint64_t& index) 27 { 28 // Convert the unique ID back to a bootID + timestamp to find the entry 29 auto underscore1Pos = entryIDStrView.find('_'); 30 if (underscore1Pos == std::string_view::npos) 31 { 32 // EntryID has no bootID or timestamp 33 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); 34 return false; 35 } 36 37 // EntryID has bootID + timestamp 38 39 // Convert entryIDViewString to BootID 40 // NOTE: bootID string which needs to be null-terminated for 41 // sd_id128_from_string() 42 std::string bootIDStr(entryIDStrView.substr(0, underscore1Pos)); 43 if (sd_id128_from_string(bootIDStr.c_str(), &bootID) < 0) 44 { 45 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); 46 return false; 47 } 48 49 // Get the timestamp from entryID 50 entryIDStrView.remove_prefix(underscore1Pos + 1); 51 52 auto [timestampEnd, tstampEc] = std::from_chars( 53 entryIDStrView.begin(), entryIDStrView.end(), timestamp); 54 if (tstampEc != std::errc()) 55 { 56 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); 57 return false; 58 } 59 entryIDStrView = std::string_view( 60 timestampEnd, 61 static_cast<size_t>(std::distance(timestampEnd, entryIDStrView.end()))); 62 if (entryIDStrView.empty()) 63 { 64 index = 0U; 65 return true; 66 } 67 // Timestamp might include optional index, if two events happened at the 68 // same "time". 69 if (entryIDStrView[0] != '_') 70 { 71 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); 72 return false; 73 } 74 entryIDStrView.remove_prefix(1); 75 auto [ptr, indexEc] = std::from_chars(entryIDStrView.begin(), 76 entryIDStrView.end(), index); 77 if (indexEc != std::errc() || ptr != entryIDStrView.end()) 78 { 79 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); 80 return false; 81 } 82 return true; 83 } 84 85 inline bool getUniqueEntryID(sd_journal* journal, std::string& entryID, 86 const bool firstEntry = true) 87 { 88 int ret = 0; 89 static sd_id128_t prevBootID{}; 90 static uint64_t prevTs = 0; 91 static int index = 0; 92 if (firstEntry) 93 { 94 prevBootID = {}; 95 prevTs = 0; 96 } 97 98 // Get the entry timestamp 99 uint64_t curTs = 0; 100 sd_id128_t curBootID{}; 101 ret = sd_journal_get_monotonic_usec(journal, &curTs, &curBootID); 102 if (ret < 0) 103 { 104 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret)); 105 return false; 106 } 107 // If the timestamp isn't unique on the same boot, increment the index 108 bool sameBootIDs = sd_id128_equal(curBootID, prevBootID) != 0; 109 if (sameBootIDs && (curTs == prevTs)) 110 { 111 index++; 112 } 113 else 114 { 115 // Otherwise, reset it 116 index = 0; 117 } 118 119 if (!sameBootIDs) 120 { 121 // Save the bootID 122 prevBootID = curBootID; 123 } 124 // Save the timestamp 125 prevTs = curTs; 126 127 // make entryID as <bootID>_<timestamp>[_<index>] 128 std::array<char, SD_ID128_STRING_MAX> bootIDStr{}; 129 sd_id128_to_string(curBootID, bootIDStr.data()); 130 entryID = std::format("{}_{}", bootIDStr.data(), curTs); 131 if (index > 0) 132 { 133 entryID += "_" + std::to_string(index); 134 } 135 return true; 136 } 137 138 inline int getJournalMetadata(sd_journal* journal, std::string_view field, 139 std::string_view& contents) 140 { 141 const char* data = nullptr; 142 size_t length = 0; 143 int ret = 0; 144 // Get the metadata from the requested field of the journal entry 145 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 146 const void** dataVoid = reinterpret_cast<const void**>(&data); 147 148 ret = sd_journal_get_data(journal, field.data(), dataVoid, &length); 149 if (ret < 0) 150 { 151 return ret; 152 } 153 contents = std::string_view(data, length); 154 // Only use the content after the "=" character. 155 contents.remove_prefix(std::min(contents.find('=') + 1, contents.size())); 156 return ret; 157 } 158 159 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field, 160 const int& base, long int& contents) 161 { 162 int ret = 0; 163 std::string_view metadata; 164 // Get the metadata from the requested field of the journal entry 165 ret = getJournalMetadata(journal, field, metadata); 166 if (ret < 0) 167 { 168 return ret; 169 } 170 contents = strtol(metadata.data(), nullptr, base); 171 return ret; 172 } 173 174 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp) 175 { 176 int ret = 0; 177 uint64_t timestamp = 0; 178 ret = sd_journal_get_realtime_usec(journal, ×tamp); 179 if (ret < 0) 180 { 181 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret)); 182 return false; 183 } 184 entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp); 185 return true; 186 } 187 188 inline int 189 fillBMCJournalLogEntryJson(const std::string& bmcJournalLogEntryID, 190 sd_journal* journal, 191 nlohmann::json::object_t& bmcJournalLogEntryJson) 192 { 193 // Get the Log Entry contents 194 int ret = 0; 195 196 std::string message; 197 std::string_view syslogID; 198 ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID); 199 if (ret < 0) 200 { 201 BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}", 202 strerror(-ret)); 203 } 204 if (!syslogID.empty()) 205 { 206 message += std::string(syslogID) + ": "; 207 } 208 209 std::string_view msg; 210 ret = getJournalMetadata(journal, "MESSAGE", msg); 211 if (ret < 0) 212 { 213 BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret)); 214 return 1; 215 } 216 message += std::string(msg); 217 218 // Get the severity from the PRIORITY field 219 long int severity = 8; // Default to an invalid priority 220 ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity); 221 if (ret < 0) 222 { 223 BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret)); 224 } 225 226 // Get the Created time from the timestamp 227 std::string entryTimeStr; 228 if (!getEntryTimestamp(journal, entryTimeStr)) 229 { 230 return 1; 231 } 232 233 // Fill in the log entry with the gathered data 234 bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 235 bmcJournalLogEntryJson["@odata.id"] = boost::urls::format( 236 "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}", 237 BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID); 238 bmcJournalLogEntryJson["Name"] = "BMC Journal Entry"; 239 bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID; 240 bmcJournalLogEntryJson["Message"] = std::move(message); 241 bmcJournalLogEntryJson["EntryType"] = "Oem"; 242 log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK; 243 if (severity <= 2) 244 { 245 severityEnum = log_entry::EventSeverity::Critical; 246 } 247 else if (severity <= 4) 248 { 249 severityEnum = log_entry::EventSeverity::Warning; 250 } 251 252 bmcJournalLogEntryJson["Severity"] = severityEnum; 253 bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry"; 254 bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr); 255 return 0; 256 } 257 258 inline void requestRoutesBMCJournalLogService(App& app) 259 { 260 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/") 261 .privileges(redfish::privileges::getLogService) 262 .methods(boost::beast::http::verb::get)( 263 [&app](const crow::Request& req, 264 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 265 const std::string& managerId) { 266 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 267 { 268 return; 269 } 270 271 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 272 { 273 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 274 return; 275 } 276 277 asyncResp->res.jsonValue["@odata.type"] = 278 "#LogService.v1_2_0.LogService"; 279 asyncResp->res.jsonValue["@odata.id"] = 280 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", 281 BMCWEB_REDFISH_MANAGER_URI_NAME); 282 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 283 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 284 asyncResp->res.jsonValue["Id"] = "Journal"; 285 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 286 287 std::pair<std::string, std::string> redfishDateTimeOffset = 288 redfish::time_utils::getDateTimeOffsetNow(); 289 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; 290 asyncResp->res.jsonValue["DateTimeLocalOffset"] = 291 redfishDateTimeOffset.second; 292 293 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( 294 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 295 BMCWEB_REDFISH_MANAGER_URI_NAME); 296 }); 297 } 298 299 inline void requestRoutesBMCJournalLogEntryCollection(App& app) 300 { 301 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/") 302 .privileges(redfish::privileges::getLogEntryCollection) 303 .methods(boost::beast::http::verb::get)( 304 [&app](const crow::Request& req, 305 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 306 const std::string& managerId) { 307 query_param::QueryCapabilities capabilities = { 308 .canDelegateTop = true, 309 .canDelegateSkip = true, 310 }; 311 query_param::Query delegatedQuery; 312 if (!redfish::setUpRedfishRouteWithDelegation( 313 app, req, asyncResp, delegatedQuery, capabilities)) 314 { 315 return; 316 } 317 318 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 319 { 320 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 321 return; 322 } 323 324 size_t skip = delegatedQuery.skip.value_or(0); 325 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 326 327 // Collections don't include the static data added by SubRoute 328 // because it has a duplicate entry for members 329 asyncResp->res.jsonValue["@odata.type"] = 330 "#LogEntryCollection.LogEntryCollection"; 331 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 332 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 333 BMCWEB_REDFISH_MANAGER_URI_NAME); 334 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 335 asyncResp->res.jsonValue["Description"] = 336 "Collection of BMC Journal Entries"; 337 nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; 338 logEntryArray = nlohmann::json::array(); 339 340 // Go through the journal and use the timestamp to create a 341 // unique ID for each entry 342 sd_journal* journalTmp = nullptr; 343 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 344 if (ret < 0) 345 { 346 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 347 messages::internalError(asyncResp->res); 348 return; 349 } 350 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 351 journalTmp, sd_journal_close); 352 journalTmp = nullptr; 353 uint64_t entryCount = 0; 354 // Reset the unique ID on the first entry 355 bool firstEntry = true; 356 SD_JOURNAL_FOREACH(journal.get()) 357 { 358 entryCount++; 359 // Handle paging using skip (number of entries to skip from 360 // the start) and top (number of entries to display) 361 if (entryCount <= skip || entryCount > skip + top) 362 { 363 continue; 364 } 365 366 std::string idStr; 367 if (!getUniqueEntryID(journal.get(), idStr, firstEntry)) 368 { 369 continue; 370 } 371 firstEntry = false; 372 373 nlohmann::json::object_t bmcJournalLogEntry; 374 if (fillBMCJournalLogEntryJson(idStr, journal.get(), 375 bmcJournalLogEntry) != 0) 376 { 377 messages::internalError(asyncResp->res); 378 return; 379 } 380 logEntryArray.emplace_back(std::move(bmcJournalLogEntry)); 381 } 382 asyncResp->res.jsonValue["Members@odata.count"] = entryCount; 383 if (skip + top < entryCount) 384 { 385 asyncResp->res 386 .jsonValue["Members@odata.nextLink"] = boost::urls::format( 387 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", 388 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); 389 } 390 }); 391 } 392 393 inline void requestRoutesBMCJournalLogEntry(App& app) 394 { 395 BMCWEB_ROUTE( 396 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/") 397 .privileges(redfish::privileges::getLogEntry) 398 .methods(boost::beast::http::verb::get)( 399 [&app](const crow::Request& req, 400 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 401 const std::string& managerId, const std::string& entryID) { 402 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 403 { 404 return; 405 } 406 407 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 408 { 409 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 410 return; 411 } 412 413 // Convert the unique ID back to a timestamp to find the entry 414 sd_id128_t bootID{}; 415 uint64_t ts = 0; 416 uint64_t index = 0; 417 if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index)) 418 { 419 return; 420 } 421 422 sd_journal* journalTmp = nullptr; 423 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 424 if (ret < 0) 425 { 426 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 427 messages::internalError(asyncResp->res); 428 return; 429 } 430 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 431 journalTmp, sd_journal_close); 432 journalTmp = nullptr; 433 // Go to the timestamp in the log and move to the entry at the 434 // index tracking the unique ID 435 std::string idStr; 436 bool firstEntry = true; 437 ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts); 438 if (ret < 0) 439 { 440 BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}", 441 strerror(-ret)); 442 messages::internalError(asyncResp->res); 443 return; 444 } 445 for (uint64_t i = 0; i <= index; i++) 446 { 447 sd_journal_next(journal.get()); 448 if (!getUniqueEntryID(journal.get(), idStr, firstEntry)) 449 { 450 messages::internalError(asyncResp->res); 451 return; 452 } 453 firstEntry = false; 454 } 455 // Confirm that the entry ID matches what was requested 456 if (idStr != entryID) 457 { 458 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 459 return; 460 } 461 462 nlohmann::json::object_t bmcJournalLogEntry; 463 if (fillBMCJournalLogEntryJson(entryID, journal.get(), 464 bmcJournalLogEntry) != 0) 465 { 466 messages::internalError(asyncResp->res); 467 return; 468 } 469 asyncResp->res.jsonValue.update(bmcJournalLogEntry); 470 }); 471 } 472 } // namespace redfish 473