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 if (index <= 1) 83 { 84 // Indexes go directly from no postfix (handled above) to _2 85 // so if we ever see _0 or _1, it's incorrect 86 messages::resourceNotFound(asyncResp->res, "LogEntry", entryIDStrView); 87 return false; 88 } 89 90 // URI indexes are one based, journald is zero based 91 index -= 1; 92 return true; 93 } 94 95 inline std::string getUniqueEntryID(uint64_t index, uint64_t curTs, 96 sd_id128_t& curBootID) 97 { 98 // make entryID as <bootID>_<timestamp>[_<index>] 99 std::array<char, SD_ID128_STRING_MAX> bootIDStr{}; 100 sd_id128_to_string(curBootID, bootIDStr.data()); 101 std::string postfix; 102 if (index > 0) 103 { 104 postfix = std::format("_{}", index + 1); 105 } 106 return std::format("{}_{}{}", bootIDStr.data(), curTs, postfix); 107 } 108 109 inline int getJournalMetadata(sd_journal* journal, std::string_view field, 110 std::string_view& contents) 111 { 112 const char* data = nullptr; 113 size_t length = 0; 114 int ret = 0; 115 // Get the metadata from the requested field of the journal entry 116 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 117 const void** dataVoid = reinterpret_cast<const void**>(&data); 118 119 ret = sd_journal_get_data(journal, field.data(), dataVoid, &length); 120 if (ret < 0) 121 { 122 return ret; 123 } 124 contents = std::string_view(data, length); 125 // Only use the content after the "=" character. 126 contents.remove_prefix(std::min(contents.find('=') + 1, contents.size())); 127 return ret; 128 } 129 130 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field, 131 const int& base, long int& contents) 132 { 133 int ret = 0; 134 std::string_view metadata; 135 // Get the metadata from the requested field of the journal entry 136 ret = getJournalMetadata(journal, field, metadata); 137 if (ret < 0) 138 { 139 return ret; 140 } 141 contents = strtol(metadata.data(), nullptr, base); 142 return ret; 143 } 144 145 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp) 146 { 147 int ret = 0; 148 uint64_t timestamp = 0; 149 ret = sd_journal_get_realtime_usec(journal, ×tamp); 150 if (ret < 0) 151 { 152 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret)); 153 return false; 154 } 155 entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp); 156 return true; 157 } 158 159 inline bool 160 fillBMCJournalLogEntryJson(const std::string& bmcJournalLogEntryID, 161 sd_journal* journal, 162 nlohmann::json::object_t& bmcJournalLogEntryJson) 163 { 164 // Get the Log Entry contents 165 std::string message; 166 std::string_view syslogID; 167 int ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID); 168 if (ret < 0) 169 { 170 BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}", 171 strerror(-ret)); 172 } 173 if (!syslogID.empty()) 174 { 175 message += std::string(syslogID) + ": "; 176 } 177 178 std::string_view msg; 179 ret = getJournalMetadata(journal, "MESSAGE", msg); 180 if (ret < 0) 181 { 182 BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret)); 183 return false; 184 } 185 message += std::string(msg); 186 187 // Get the severity from the PRIORITY field 188 long int severity = 8; // Default to an invalid priority 189 ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity); 190 if (ret < 0) 191 { 192 BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret)); 193 } 194 195 // Get the Created time from the timestamp 196 std::string entryTimeStr; 197 if (!getEntryTimestamp(journal, entryTimeStr)) 198 { 199 return false; 200 } 201 202 // Fill in the log entry with the gathered data 203 bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 204 bmcJournalLogEntryJson["@odata.id"] = boost::urls::format( 205 "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}", 206 BMCWEB_REDFISH_MANAGER_URI_NAME, bmcJournalLogEntryID); 207 bmcJournalLogEntryJson["Name"] = "BMC Journal Entry"; 208 bmcJournalLogEntryJson["Id"] = bmcJournalLogEntryID; 209 bmcJournalLogEntryJson["Message"] = std::move(message); 210 bmcJournalLogEntryJson["EntryType"] = "Oem"; 211 log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK; 212 if (severity <= 2) 213 { 214 severityEnum = log_entry::EventSeverity::Critical; 215 } 216 else if (severity <= 4) 217 { 218 severityEnum = log_entry::EventSeverity::Warning; 219 } 220 221 bmcJournalLogEntryJson["Severity"] = severityEnum; 222 bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry"; 223 bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr); 224 return true; 225 } 226 227 inline void handleManagersLogServiceJournalGet( 228 App& app, const crow::Request& req, 229 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 230 const std::string& managerId) 231 { 232 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 233 { 234 return; 235 } 236 237 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 238 { 239 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 240 return; 241 } 242 243 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 244 asyncResp->res.jsonValue["@odata.id"] = 245 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", 246 BMCWEB_REDFISH_MANAGER_URI_NAME); 247 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 248 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 249 asyncResp->res.jsonValue["Id"] = "Journal"; 250 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 251 252 std::pair<std::string, std::string> redfishDateTimeOffset = 253 redfish::time_utils::getDateTimeOffsetNow(); 254 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; 255 asyncResp->res.jsonValue["DateTimeLocalOffset"] = 256 redfishDateTimeOffset.second; 257 258 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( 259 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 260 BMCWEB_REDFISH_MANAGER_URI_NAME); 261 } 262 263 struct JournalReadState 264 { 265 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal; 266 uint64_t index = 0; 267 sd_id128_t prevBootID{}; 268 uint64_t prevTs = 0; 269 }; 270 271 inline void 272 readJournalEntries(uint64_t topEntryCount, 273 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 274 JournalReadState&& readState) 275 { 276 nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"]; 277 nlohmann::json::array_t* logEntryArray = 278 logEntry.get_ptr<nlohmann::json::array_t*>(); 279 if (logEntryArray == nullptr) 280 { 281 messages::internalError(asyncResp->res); 282 return; 283 } 284 285 // The Journal APIs unfortunately do blocking calls to the filesystem, and 286 // can be somewhat expensive. Short of creating our own io_uring based 287 // implementation of sd-journal, which would be difficult, the best thing we 288 // can do is to only parse a certain number of entries at a time. The 289 // current chunk size is selected arbitrarily to ensure that we're not 290 // trying to process thousands of entries at the same time. 291 // The implementation will process the number of entries, then return 292 // control to the io_context to let other operations continue. 293 size_t segmentCountRemaining = 10; 294 295 // Reset the unique ID on the first entry 296 for (uint64_t entryCount = logEntryArray->size(); 297 entryCount < topEntryCount; entryCount++) 298 { 299 if (segmentCountRemaining == 0) 300 { 301 boost::asio::post(crow::connections::systemBus->get_io_context(), 302 [asyncResp, topEntryCount, 303 readState = std::move(readState)]() mutable { 304 readJournalEntries(topEntryCount, asyncResp, 305 std::move(readState)); 306 }); 307 return; 308 } 309 310 // Get the entry timestamp 311 sd_id128_t curBootID{}; 312 uint64_t curTs = 0; 313 int ret = sd_journal_get_monotonic_usec(readState.journal.get(), &curTs, 314 &curBootID); 315 if (ret < 0) 316 { 317 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", 318 strerror(-ret)); 319 messages::internalError(asyncResp->res); 320 return; 321 } 322 323 // If the timestamp isn't unique on the same boot, increment the index 324 bool sameBootIDs = sd_id128_equal(curBootID, readState.prevBootID) != 0; 325 if (sameBootIDs && (curTs == readState.prevTs)) 326 { 327 readState.index++; 328 } 329 else 330 { 331 // Otherwise, reset it 332 readState.index = 0; 333 } 334 335 // Save the bootID 336 readState.prevBootID = curBootID; 337 338 // Save the timestamp 339 readState.prevTs = curTs; 340 341 std::string idStr = getUniqueEntryID(readState.index, curTs, curBootID); 342 343 nlohmann::json::object_t bmcJournalLogEntry; 344 if (!fillBMCJournalLogEntryJson(idStr, readState.journal.get(), 345 bmcJournalLogEntry)) 346 { 347 messages::internalError(asyncResp->res); 348 return; 349 } 350 logEntryArray->emplace_back(std::move(bmcJournalLogEntry)); 351 352 ret = sd_journal_next(readState.journal.get()); 353 if (ret < 0) 354 { 355 messages::internalError(asyncResp->res); 356 return; 357 } 358 if (ret == 0) 359 { 360 break; 361 } 362 segmentCountRemaining--; 363 } 364 } 365 366 inline void handleManagersJournalLogEntryCollectionGet( 367 App& app, const crow::Request& req, 368 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 369 const std::string& managerId) 370 { 371 query_param::QueryCapabilities capabilities = { 372 .canDelegateTop = true, 373 .canDelegateSkip = true, 374 }; 375 query_param::Query delegatedQuery; 376 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 377 delegatedQuery, capabilities)) 378 { 379 return; 380 } 381 382 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 383 { 384 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 385 return; 386 } 387 388 size_t skip = delegatedQuery.skip.value_or(0); 389 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 390 391 // Collections don't include the static data added by SubRoute 392 // because it has a duplicate entry for members 393 asyncResp->res.jsonValue["@odata.type"] = 394 "#LogEntryCollection.LogEntryCollection"; 395 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 396 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 397 BMCWEB_REDFISH_MANAGER_URI_NAME); 398 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 399 asyncResp->res.jsonValue["Description"] = 400 "Collection of BMC Journal Entries"; 401 asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t(); 402 403 // Go through the journal and use the timestamp to create a 404 // unique ID for each entry 405 sd_journal* journalTmp = nullptr; 406 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 407 if (ret < 0) 408 { 409 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 410 messages::internalError(asyncResp->res); 411 return; 412 } 413 414 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 415 journalTmp, sd_journal_close); 416 journalTmp = nullptr; 417 418 // Seek to the end 419 if (sd_journal_seek_tail(journal.get()) < 0) 420 { 421 messages::internalError(asyncResp->res); 422 return; 423 } 424 425 // Get the last entry 426 if (sd_journal_previous(journal.get()) < 0) 427 { 428 messages::internalError(asyncResp->res); 429 return; 430 } 431 432 // Get the last sequence number 433 uint64_t endSeqNum = 0; 434 #if SYSTEMD_VERSION >= 254 435 { 436 if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0) 437 { 438 messages::internalError(asyncResp->res); 439 return; 440 } 441 } 442 #endif 443 444 // Seek to the beginning 445 if (sd_journal_seek_head(journal.get()) < 0) 446 { 447 messages::internalError(asyncResp->res); 448 return; 449 } 450 451 // Get the first entry 452 if (sd_journal_next(journal.get()) < 0) 453 { 454 messages::internalError(asyncResp->res); 455 return; 456 } 457 458 // Get the first sequence number 459 uint64_t startSeqNum = 0; 460 #if SYSTEMD_VERSION >= 254 461 { 462 if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0) 463 { 464 messages::internalError(asyncResp->res); 465 return; 466 } 467 } 468 #endif 469 470 BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum, 471 endSeqNum); 472 473 // Add 1 to account for the last entry 474 uint64_t totalEntries = endSeqNum - startSeqNum + 1; 475 asyncResp->res.jsonValue["Members@odata.count"] = totalEntries; 476 if (skip + top < totalEntries) 477 { 478 asyncResp->res.jsonValue["Members@odata.nextLink"] = 479 boost::urls::format( 480 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", 481 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); 482 } 483 uint64_t index = 0; 484 sd_id128_t curBootID{}; 485 uint64_t curTs = 0; 486 if (skip > 0) 487 { 488 if (sd_journal_next_skip(journal.get(), skip) < 0) 489 { 490 messages::internalError(asyncResp->res); 491 return; 492 } 493 494 // Get the entry timestamp 495 ret = sd_journal_get_monotonic_usec(journal.get(), &curTs, &curBootID); 496 if (ret < 0) 497 { 498 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", 499 strerror(-ret)); 500 messages::internalError(asyncResp->res); 501 return; 502 } 503 504 uint64_t endChunkSeqNum = 0; 505 #if SYSTEMD_VERSION >= 254 506 { 507 if (sd_journal_get_seqnum(journal.get(), &endChunkSeqNum, nullptr) < 508 0) 509 { 510 messages::internalError(asyncResp->res); 511 return; 512 } 513 } 514 #endif 515 516 // Seek to the first entry with the same timestamp and boot 517 ret = sd_journal_seek_monotonic_usec(journal.get(), curBootID, curTs); 518 if (ret < 0) 519 { 520 BMCWEB_LOG_ERROR("Failed to seek: {}", strerror(-ret)); 521 messages::internalError(asyncResp->res); 522 return; 523 } 524 if (sd_journal_next(journal.get()) < 0) 525 { 526 messages::internalError(asyncResp->res); 527 return; 528 } 529 uint64_t startChunkSeqNum = 0; 530 #if SYSTEMD_VERSION >= 254 531 { 532 if (sd_journal_get_seqnum(journal.get(), &startChunkSeqNum, 533 nullptr) < 0) 534 { 535 messages::internalError(asyncResp->res); 536 return; 537 } 538 } 539 #endif 540 541 // Get the difference between the start and end. Most of the time this 542 // will be 0 543 BMCWEB_LOG_DEBUG("start={} end={}", startChunkSeqNum, endChunkSeqNum); 544 index = endChunkSeqNum - startChunkSeqNum; 545 if (index > endChunkSeqNum) 546 { 547 // Detect underflows. Should never happen. 548 messages::internalError(asyncResp->res); 549 return; 550 } 551 if (index > 0) 552 { 553 BMCWEB_LOG_DEBUG("index = {}", index); 554 if (sd_journal_next_skip(journal.get(), index) < 0) 555 { 556 messages::internalError(asyncResp->res); 557 return; 558 } 559 } 560 } 561 // If this is the first entry of this series, reset the timestamps so the 562 // Index doesn't increment 563 if (index == 0) 564 { 565 curBootID = {}; 566 curTs = 0; 567 } 568 else 569 { 570 index -= 1; 571 } 572 BMCWEB_LOG_DEBUG("Index was {}", index); 573 readJournalEntries(top, asyncResp, 574 {std::move(journal), index, curBootID, curTs}); 575 } 576 577 inline void handleManagersJournalEntriesLogEntryGet( 578 App& app, const crow::Request& req, 579 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 580 const std::string& managerId, const std::string& entryID) 581 { 582 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 583 { 584 return; 585 } 586 587 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 588 { 589 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 590 return; 591 } 592 593 // Convert the unique ID back to a timestamp to find the entry 594 sd_id128_t bootID{}; 595 uint64_t ts = 0; 596 uint64_t index = 0; 597 if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index)) 598 { 599 return; 600 } 601 602 sd_journal* journalTmp = nullptr; 603 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 604 if (ret < 0) 605 { 606 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 607 messages::internalError(asyncResp->res); 608 return; 609 } 610 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 611 journalTmp, sd_journal_close); 612 journalTmp = nullptr; 613 // Go to the timestamp in the log and move to the entry at the 614 // index tracking the unique ID 615 ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts); 616 if (ret < 0) 617 { 618 BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}", 619 strerror(-ret)); 620 messages::internalError(asyncResp->res); 621 return; 622 } 623 624 if (sd_journal_next_skip(journal.get(), index + 1) < 0) 625 { 626 messages::internalError(asyncResp->res); 627 return; 628 } 629 630 std::string idStr = getUniqueEntryID(index, ts, bootID); 631 632 nlohmann::json::object_t bmcJournalLogEntry; 633 if (!fillBMCJournalLogEntryJson(entryID, journal.get(), bmcJournalLogEntry)) 634 { 635 messages::internalError(asyncResp->res); 636 return; 637 } 638 asyncResp->res.jsonValue.update(bmcJournalLogEntry); 639 } 640 641 inline void requestRoutesBMCJournalLogService(App& app) 642 { 643 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/") 644 .privileges(redfish::privileges::getLogService) 645 .methods(boost::beast::http::verb::get)( 646 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app))); 647 648 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/") 649 .privileges(redfish::privileges::getLogEntryCollection) 650 .methods(boost::beast::http::verb::get)(std::bind_front( 651 handleManagersJournalLogEntryCollectionGet, std::ref(app))); 652 653 BMCWEB_ROUTE( 654 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/") 655 .privileges(redfish::privileges::getLogEntry) 656 .methods(boost::beast::http::verb::get)(std::bind_front( 657 handleManagersJournalEntriesLogEntryGet, std::ref(app))); 658 } 659 } // namespace redfish 660