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 handleManagersLogServiceJournalGet( 259 App& app, const crow::Request& req, 260 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 261 const std::string& managerId) 262 { 263 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 264 { 265 return; 266 } 267 268 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 269 { 270 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 271 return; 272 } 273 274 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 275 asyncResp->res.jsonValue["@odata.id"] = 276 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", 277 BMCWEB_REDFISH_MANAGER_URI_NAME); 278 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 279 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 280 asyncResp->res.jsonValue["Id"] = "Journal"; 281 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 282 283 std::pair<std::string, std::string> redfishDateTimeOffset = 284 redfish::time_utils::getDateTimeOffsetNow(); 285 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; 286 asyncResp->res.jsonValue["DateTimeLocalOffset"] = 287 redfishDateTimeOffset.second; 288 289 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( 290 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 291 BMCWEB_REDFISH_MANAGER_URI_NAME); 292 } 293 294 inline void handleManagersJournalLogEntryCollectionGet( 295 App& app, const crow::Request& req, 296 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 297 const std::string& managerId) 298 { 299 query_param::QueryCapabilities capabilities = { 300 .canDelegateTop = true, 301 .canDelegateSkip = true, 302 }; 303 query_param::Query delegatedQuery; 304 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 305 delegatedQuery, capabilities)) 306 { 307 return; 308 } 309 310 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 311 { 312 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 313 return; 314 } 315 316 size_t skip = delegatedQuery.skip.value_or(0); 317 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 318 319 // Collections don't include the static data added by SubRoute 320 // because it has a duplicate entry for members 321 asyncResp->res.jsonValue["@odata.type"] = 322 "#LogEntryCollection.LogEntryCollection"; 323 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 324 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 325 BMCWEB_REDFISH_MANAGER_URI_NAME); 326 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 327 asyncResp->res.jsonValue["Description"] = 328 "Collection of BMC Journal Entries"; 329 nlohmann::json& logEntryArray = asyncResp->res.jsonValue["Members"]; 330 logEntryArray = nlohmann::json::array(); 331 332 // Go through the journal and use the timestamp to create a 333 // unique ID for each entry 334 sd_journal* journalTmp = nullptr; 335 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 336 if (ret < 0) 337 { 338 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 339 messages::internalError(asyncResp->res); 340 return; 341 } 342 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 343 journalTmp, sd_journal_close); 344 journalTmp = nullptr; 345 uint64_t entryCount = 0; 346 // Reset the unique ID on the first entry 347 bool firstEntry = true; 348 SD_JOURNAL_FOREACH(journal.get()) 349 { 350 entryCount++; 351 // Handle paging using skip (number of entries to skip from 352 // the start) and top (number of entries to display) 353 if (entryCount <= skip || entryCount > skip + top) 354 { 355 continue; 356 } 357 358 std::string idStr; 359 if (!getUniqueEntryID(journal.get(), idStr, firstEntry)) 360 { 361 continue; 362 } 363 firstEntry = false; 364 365 nlohmann::json::object_t bmcJournalLogEntry; 366 if (fillBMCJournalLogEntryJson(idStr, journal.get(), 367 bmcJournalLogEntry) != 0) 368 { 369 messages::internalError(asyncResp->res); 370 return; 371 } 372 logEntryArray.emplace_back(std::move(bmcJournalLogEntry)); 373 } 374 asyncResp->res.jsonValue["Members@odata.count"] = entryCount; 375 if (skip + top < entryCount) 376 { 377 asyncResp->res.jsonValue["Members@odata.nextLink"] = 378 boost::urls::format( 379 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", 380 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); 381 } 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 // Convert the unique ID back to a timestamp to find the entry 401 sd_id128_t bootID{}; 402 uint64_t ts = 0; 403 uint64_t index = 0; 404 if (!getTimestampFromID(asyncResp, entryID, bootID, ts, index)) 405 { 406 return; 407 } 408 409 sd_journal* journalTmp = nullptr; 410 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 411 if (ret < 0) 412 { 413 BMCWEB_LOG_ERROR("failed to open journal: {}", strerror(-ret)); 414 messages::internalError(asyncResp->res); 415 return; 416 } 417 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 418 journalTmp, sd_journal_close); 419 journalTmp = nullptr; 420 // Go to the timestamp in the log and move to the entry at the 421 // index tracking the unique ID 422 std::string idStr; 423 bool firstEntry = true; 424 ret = sd_journal_seek_monotonic_usec(journal.get(), bootID, ts); 425 if (ret < 0) 426 { 427 BMCWEB_LOG_ERROR("failed to seek to an entry in journal{}", 428 strerror(-ret)); 429 messages::internalError(asyncResp->res); 430 return; 431 } 432 for (uint64_t i = 0; i <= index; i++) 433 { 434 sd_journal_next(journal.get()); 435 if (!getUniqueEntryID(journal.get(), idStr, firstEntry)) 436 { 437 messages::internalError(asyncResp->res); 438 return; 439 } 440 firstEntry = false; 441 } 442 // Confirm that the entry ID matches what was requested 443 if (idStr != entryID) 444 { 445 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 446 return; 447 } 448 449 nlohmann::json::object_t bmcJournalLogEntry; 450 if (fillBMCJournalLogEntryJson(entryID, journal.get(), 451 bmcJournalLogEntry) != 0) 452 { 453 messages::internalError(asyncResp->res); 454 return; 455 } 456 asyncResp->res.jsonValue.update(bmcJournalLogEntry); 457 }; 458 459 inline void requestRoutesBMCJournalLogService(App& app) 460 { 461 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/") 462 .privileges(redfish::privileges::getLogService) 463 .methods(boost::beast::http::verb::get)( 464 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app))); 465 466 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/") 467 .privileges(redfish::privileges::getLogEntryCollection) 468 .methods(boost::beast::http::verb::get)(std::bind_front( 469 handleManagersJournalLogEntryCollectionGet, std::ref(app))); 470 471 BMCWEB_ROUTE( 472 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/") 473 .privileges(redfish::privileges::getLogEntry) 474 .methods(boost::beast::http::verb::get)(std::bind_front( 475 handleManagersJournalEntriesLogEntryGet, std::ref(app))); 476 } 477 } // namespace redfish 478