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