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