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 23 inline int getJournalMetadata(sd_journal* journal, std::string_view field, 24 std::string_view& contents) 25 { 26 const char* data = nullptr; 27 size_t length = 0; 28 int ret = 0; 29 // Get the metadata from the requested field of the journal entry 30 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 31 const void** dataVoid = reinterpret_cast<const void**>(&data); 32 33 ret = sd_journal_get_data(journal, field.data(), dataVoid, &length); 34 if (ret < 0) 35 { 36 return ret; 37 } 38 contents = std::string_view(data, length); 39 // Only use the content after the "=" character. 40 contents.remove_prefix(std::min(contents.find('=') + 1, contents.size())); 41 return ret; 42 } 43 44 inline int getJournalMetadataInt(sd_journal* journal, std::string_view field, 45 const int& base, long int& contents) 46 { 47 int ret = 0; 48 std::string_view metadata; 49 // Get the metadata from the requested field of the journal entry 50 ret = getJournalMetadata(journal, field, metadata); 51 if (ret < 0) 52 { 53 return ret; 54 } 55 contents = strtol(metadata.data(), nullptr, base); 56 return ret; 57 } 58 59 inline bool getEntryTimestamp(sd_journal* journal, std::string& entryTimestamp) 60 { 61 int ret = 0; 62 uint64_t timestamp = 0; 63 ret = sd_journal_get_realtime_usec(journal, ×tamp); 64 if (ret < 0) 65 { 66 BMCWEB_LOG_ERROR("Failed to read entry timestamp: {}", strerror(-ret)); 67 return false; 68 } 69 entryTimestamp = redfish::time_utils::getDateTimeUintUs(timestamp); 70 return true; 71 } 72 73 inline bool fillBMCJournalLogEntryJson( 74 sd_journal* journal, nlohmann::json::object_t& bmcJournalLogEntryJson) 75 { 76 char* cursor = nullptr; 77 int ret = sd_journal_get_cursor(journal, &cursor); 78 if (ret < 0) 79 { 80 return false; 81 } 82 std::unique_ptr<char*> cursorptr = std::make_unique<char*>(cursor); 83 std::string bmcJournalLogEntryID(cursor); 84 85 // Get the Log Entry contents 86 std::string message; 87 std::string_view syslogID; 88 ret = getJournalMetadata(journal, "SYSLOG_IDENTIFIER", syslogID); 89 if (ret < 0) 90 { 91 BMCWEB_LOG_DEBUG("Failed to read SYSLOG_IDENTIFIER field: {}", 92 strerror(-ret)); 93 } 94 if (!syslogID.empty()) 95 { 96 message += std::string(syslogID) + ": "; 97 } 98 99 std::string_view msg; 100 ret = getJournalMetadata(journal, "MESSAGE", msg); 101 if (ret < 0) 102 { 103 BMCWEB_LOG_ERROR("Failed to read MESSAGE field: {}", strerror(-ret)); 104 return false; 105 } 106 message += std::string(msg); 107 108 // Get the severity from the PRIORITY field 109 long int severity = 8; // Default to an invalid priority 110 ret = getJournalMetadataInt(journal, "PRIORITY", 10, severity); 111 if (ret < 0) 112 { 113 BMCWEB_LOG_DEBUG("Failed to read PRIORITY field: {}", strerror(-ret)); 114 } 115 116 // Get the Created time from the timestamp 117 std::string entryTimeStr; 118 if (!getEntryTimestamp(journal, entryTimeStr)) 119 { 120 return false; 121 } 122 123 // Fill in the log entry with the gathered data 124 bmcJournalLogEntryJson["@odata.type"] = "#LogEntry.v1_9_0.LogEntry"; 125 126 std::string entryIdBase64 = 127 crow::utility::base64encode(bmcJournalLogEntryID); 128 129 bmcJournalLogEntryJson["@odata.id"] = boost::urls::format( 130 "/redfish/v1/Managers/{}/LogServices/Journal/Entries/{}", 131 BMCWEB_REDFISH_MANAGER_URI_NAME, entryIdBase64); 132 bmcJournalLogEntryJson["Name"] = "BMC Journal Entry"; 133 bmcJournalLogEntryJson["Id"] = entryIdBase64; 134 bmcJournalLogEntryJson["Message"] = std::move(message); 135 bmcJournalLogEntryJson["EntryType"] = log_entry::LogEntryType::Oem; 136 log_entry::EventSeverity severityEnum = log_entry::EventSeverity::OK; 137 if (severity <= 2) 138 { 139 severityEnum = log_entry::EventSeverity::Critical; 140 } 141 else if (severity <= 4) 142 { 143 severityEnum = log_entry::EventSeverity::Warning; 144 } 145 146 bmcJournalLogEntryJson["Severity"] = severityEnum; 147 bmcJournalLogEntryJson["OemRecordFormat"] = "BMC Journal Entry"; 148 bmcJournalLogEntryJson["Created"] = std::move(entryTimeStr); 149 return true; 150 } 151 152 inline void handleManagersLogServiceJournalGet( 153 App& app, const crow::Request& req, 154 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 155 const std::string& managerId) 156 { 157 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 158 { 159 return; 160 } 161 162 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 163 { 164 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 165 return; 166 } 167 168 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 169 asyncResp->res.jsonValue["@odata.id"] = 170 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", 171 BMCWEB_REDFISH_MANAGER_URI_NAME); 172 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 173 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 174 asyncResp->res.jsonValue["Id"] = "Journal"; 175 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 176 177 std::pair<std::string, std::string> redfishDateTimeOffset = 178 redfish::time_utils::getDateTimeOffsetNow(); 179 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; 180 asyncResp->res.jsonValue["DateTimeLocalOffset"] = 181 redfishDateTimeOffset.second; 182 183 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( 184 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 185 BMCWEB_REDFISH_MANAGER_URI_NAME); 186 } 187 188 struct JournalReadState 189 { 190 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal; 191 }; 192 193 inline void readJournalEntries( 194 uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 195 JournalReadState&& readState) 196 { 197 nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"]; 198 nlohmann::json::array_t* logEntryArray = 199 logEntry.get_ptr<nlohmann::json::array_t*>(); 200 if (logEntryArray == nullptr) 201 { 202 messages::internalError(asyncResp->res); 203 return; 204 } 205 206 // The Journal APIs unfortunately do blocking calls to the filesystem, and 207 // can be somewhat expensive. Short of creating our own io_uring based 208 // implementation of sd-journal, which would be difficult, the best thing we 209 // can do is to only parse a certain number of entries at a time. The 210 // current chunk size is selected arbitrarily to ensure that we're not 211 // trying to process thousands of entries at the same time. 212 // The implementation will process the number of entries, then return 213 // control to the io_context to let other operations continue. 214 size_t segmentCountRemaining = 10; 215 216 // Reset the unique ID on the first entry 217 for (uint64_t entryCount = logEntryArray->size(); 218 entryCount < topEntryCount; entryCount++) 219 { 220 if (segmentCountRemaining == 0) 221 { 222 boost::asio::post(crow::connections::systemBus->get_io_context(), 223 [asyncResp, topEntryCount, 224 readState = std::move(readState)]() mutable { 225 readJournalEntries(topEntryCount, asyncResp, 226 std::move(readState)); 227 }); 228 return; 229 } 230 231 nlohmann::json::object_t bmcJournalLogEntry; 232 if (!fillBMCJournalLogEntryJson(readState.journal.get(), 233 bmcJournalLogEntry)) 234 { 235 messages::internalError(asyncResp->res); 236 return; 237 } 238 logEntryArray->emplace_back(std::move(bmcJournalLogEntry)); 239 240 int ret = sd_journal_next(readState.journal.get()); 241 if (ret < 0) 242 { 243 messages::internalError(asyncResp->res); 244 return; 245 } 246 if (ret == 0) 247 { 248 break; 249 } 250 segmentCountRemaining--; 251 } 252 } 253 254 inline void handleManagersJournalLogEntryCollectionGet( 255 App& app, const crow::Request& req, 256 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 257 const std::string& managerId) 258 { 259 query_param::QueryCapabilities capabilities = { 260 .canDelegateTop = true, 261 .canDelegateSkip = true, 262 }; 263 query_param::Query delegatedQuery; 264 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 265 delegatedQuery, capabilities)) 266 { 267 return; 268 } 269 270 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 271 { 272 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 273 return; 274 } 275 276 size_t skip = delegatedQuery.skip.value_or(0); 277 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 278 279 // Collections don't include the static data added by SubRoute 280 // because it has a duplicate entry for members 281 asyncResp->res.jsonValue["@odata.type"] = 282 "#LogEntryCollection.LogEntryCollection"; 283 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 284 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 285 BMCWEB_REDFISH_MANAGER_URI_NAME); 286 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 287 asyncResp->res.jsonValue["Description"] = 288 "Collection of BMC Journal Entries"; 289 asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t(); 290 291 // Go through the journal and use the timestamp to create a 292 // unique ID for each entry 293 sd_journal* journalTmp = nullptr; 294 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 295 if (ret < 0) 296 { 297 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 298 messages::internalError(asyncResp->res); 299 return; 300 } 301 302 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 303 journalTmp, sd_journal_close); 304 journalTmp = nullptr; 305 306 // Seek to the end 307 if (sd_journal_seek_tail(journal.get()) < 0) 308 { 309 messages::internalError(asyncResp->res); 310 return; 311 } 312 313 // Get the last entry 314 if (sd_journal_previous(journal.get()) < 0) 315 { 316 messages::internalError(asyncResp->res); 317 return; 318 } 319 320 // Get the last sequence number 321 uint64_t endSeqNum = 0; 322 #if LIBSYSTEMD_VERSION >= 254 323 { 324 if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0) 325 { 326 messages::internalError(asyncResp->res); 327 return; 328 } 329 } 330 #endif 331 332 // Seek to the beginning 333 if (sd_journal_seek_head(journal.get()) < 0) 334 { 335 messages::internalError(asyncResp->res); 336 return; 337 } 338 339 // Get the first entry 340 if (sd_journal_next(journal.get()) < 0) 341 { 342 messages::internalError(asyncResp->res); 343 return; 344 } 345 346 // Get the first sequence number 347 uint64_t startSeqNum = 0; 348 #if LIBSYSTEMD_VERSION >= 254 349 { 350 if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0) 351 { 352 messages::internalError(asyncResp->res); 353 return; 354 } 355 } 356 #endif 357 358 BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum, 359 endSeqNum); 360 361 // Add 1 to account for the last entry 362 uint64_t totalEntries = endSeqNum - startSeqNum + 1; 363 asyncResp->res.jsonValue["Members@odata.count"] = totalEntries; 364 if (skip + top < totalEntries) 365 { 366 asyncResp->res.jsonValue["Members@odata.nextLink"] = 367 boost::urls::format( 368 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", 369 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); 370 } 371 uint64_t index = 0; 372 if (skip > 0) 373 { 374 if (sd_journal_next_skip(journal.get(), skip) < 0) 375 { 376 messages::internalError(asyncResp->res); 377 return; 378 } 379 } 380 BMCWEB_LOG_DEBUG("Index was {}", index); 381 readJournalEntries(top, asyncResp, {std::move(journal)}); 382 } 383 384 inline void handleManagersJournalEntriesLogEntryGet( 385 App& app, const crow::Request& req, 386 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 387 const std::string& managerId, const std::string& entryID) 388 { 389 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 390 { 391 return; 392 } 393 394 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 395 { 396 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 397 return; 398 } 399 400 sd_journal* journalTmp = nullptr; 401 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 402 if (ret < 0) 403 { 404 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 405 messages::internalError(asyncResp->res); 406 return; 407 } 408 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 409 journalTmp, sd_journal_close); 410 journalTmp = nullptr; 411 412 std::string cursor; 413 if (!crow::utility::base64Decode(entryID, cursor)) 414 { 415 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 416 return; 417 } 418 419 // Go to the cursor in the log 420 ret = sd_journal_seek_cursor(journal.get(), cursor.c_str()); 421 if (ret < 0) 422 { 423 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 424 return; 425 } 426 427 if (sd_journal_next(journal.get()) < 0) 428 { 429 messages::internalError(asyncResp->res); 430 return; 431 } 432 433 ret = sd_journal_test_cursor(journal.get(), cursor.c_str()); 434 if (ret == 0) 435 { 436 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 437 return; 438 } 439 if (ret < 0) 440 { 441 messages::internalError(asyncResp->res); 442 return; 443 } 444 445 nlohmann::json::object_t bmcJournalLogEntry; 446 if (!fillBMCJournalLogEntryJson(journal.get(), bmcJournalLogEntry)) 447 { 448 messages::internalError(asyncResp->res); 449 return; 450 } 451 asyncResp->res.jsonValue.update(bmcJournalLogEntry); 452 } 453 454 inline void requestRoutesBMCJournalLogService(App& app) 455 { 456 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/") 457 .privileges(redfish::privileges::getLogService) 458 .methods(boost::beast::http::verb::get)( 459 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app))); 460 461 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/") 462 .privileges(redfish::privileges::getLogEntryCollection) 463 .methods(boost::beast::http::verb::get)(std::bind_front( 464 handleManagersJournalLogEntryCollectionGet, std::ref(app))); 465 466 BMCWEB_ROUTE( 467 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/") 468 .privileges(redfish::privileges::getLogEntry) 469 .methods(boost::beast::http::verb::get)(std::bind_front( 470 handleManagersJournalEntriesLogEntryGet, std::ref(app))); 471 } 472 } // namespace redfish 473