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/journal_utils.hpp" 10 #include "utils/time_utils.hpp" 11 12 #include <systemd/sd-journal.h> 13 14 #include <boost/beast/http/verb.hpp> 15 16 #include <array> 17 #include <memory> 18 #include <string> 19 #include <string_view> 20 21 namespace redfish 22 { 23 24 inline void handleManagersLogServiceJournalGet( 25 App& app, const crow::Request& req, 26 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 27 const std::string& managerId) 28 { 29 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 30 { 31 return; 32 } 33 34 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 35 { 36 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 37 return; 38 } 39 40 asyncResp->res.jsonValue["@odata.type"] = "#LogService.v1_2_0.LogService"; 41 asyncResp->res.jsonValue["@odata.id"] = 42 boost::urls::format("/redfish/v1/Managers/{}/LogServices/Journal", 43 BMCWEB_REDFISH_MANAGER_URI_NAME); 44 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Log Service"; 45 asyncResp->res.jsonValue["Description"] = "BMC Journal Log Service"; 46 asyncResp->res.jsonValue["Id"] = "Journal"; 47 asyncResp->res.jsonValue["OverWritePolicy"] = "WrapsWhenFull"; 48 49 std::pair<std::string, std::string> redfishDateTimeOffset = 50 redfish::time_utils::getDateTimeOffsetNow(); 51 asyncResp->res.jsonValue["DateTime"] = redfishDateTimeOffset.first; 52 asyncResp->res.jsonValue["DateTimeLocalOffset"] = 53 redfishDateTimeOffset.second; 54 55 asyncResp->res.jsonValue["Entries"]["@odata.id"] = boost::urls::format( 56 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 57 BMCWEB_REDFISH_MANAGER_URI_NAME); 58 } 59 60 struct JournalReadState 61 { 62 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal; 63 }; 64 65 inline void readJournalEntries( 66 uint64_t topEntryCount, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 67 JournalReadState&& readState) 68 { 69 nlohmann::json& logEntry = asyncResp->res.jsonValue["Members"]; 70 nlohmann::json::array_t* logEntryArray = 71 logEntry.get_ptr<nlohmann::json::array_t*>(); 72 if (logEntryArray == nullptr) 73 { 74 messages::internalError(asyncResp->res); 75 return; 76 } 77 78 // The Journal APIs unfortunately do blocking calls to the filesystem, and 79 // can be somewhat expensive. Short of creating our own io_uring based 80 // implementation of sd-journal, which would be difficult, the best thing we 81 // can do is to only parse a certain number of entries at a time. The 82 // current chunk size is selected arbitrarily to ensure that we're not 83 // trying to process thousands of entries at the same time. 84 // The implementation will process the number of entries, then return 85 // control to the io_context to let other operations continue. 86 size_t segmentCountRemaining = 10; 87 88 // Reset the unique ID on the first entry 89 for (uint64_t entryCount = logEntryArray->size(); 90 entryCount < topEntryCount; entryCount++) 91 { 92 if (segmentCountRemaining == 0) 93 { 94 boost::asio::post(crow::connections::systemBus->get_io_context(), 95 [asyncResp, topEntryCount, 96 readState = std::move(readState)]() mutable { 97 readJournalEntries(topEntryCount, asyncResp, 98 std::move(readState)); 99 }); 100 return; 101 } 102 103 nlohmann::json::object_t bmcJournalLogEntry; 104 if (!fillBMCJournalLogEntryJson(readState.journal.get(), 105 bmcJournalLogEntry)) 106 { 107 messages::internalError(asyncResp->res); 108 return; 109 } 110 logEntryArray->emplace_back(std::move(bmcJournalLogEntry)); 111 112 int ret = sd_journal_next(readState.journal.get()); 113 if (ret < 0) 114 { 115 messages::internalError(asyncResp->res); 116 return; 117 } 118 if (ret == 0) 119 { 120 break; 121 } 122 segmentCountRemaining--; 123 } 124 } 125 126 inline void handleManagersJournalLogEntryCollectionGet( 127 App& app, const crow::Request& req, 128 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 129 const std::string& managerId) 130 { 131 query_param::QueryCapabilities capabilities = { 132 .canDelegateTop = true, 133 .canDelegateSkip = true, 134 }; 135 query_param::Query delegatedQuery; 136 if (!redfish::setUpRedfishRouteWithDelegation(app, req, asyncResp, 137 delegatedQuery, capabilities)) 138 { 139 return; 140 } 141 142 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 143 { 144 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 145 return; 146 } 147 148 size_t skip = delegatedQuery.skip.value_or(0); 149 size_t top = delegatedQuery.top.value_or(query_param::Query::maxTop); 150 151 // Collections don't include the static data added by SubRoute 152 // because it has a duplicate entry for members 153 asyncResp->res.jsonValue["@odata.type"] = 154 "#LogEntryCollection.LogEntryCollection"; 155 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 156 "/redfish/v1/Managers/{}/LogServices/Journal/Entries", 157 BMCWEB_REDFISH_MANAGER_URI_NAME); 158 asyncResp->res.jsonValue["Name"] = "Open BMC Journal Entries"; 159 asyncResp->res.jsonValue["Description"] = 160 "Collection of BMC Journal Entries"; 161 asyncResp->res.jsonValue["Members"] = nlohmann::json::array_t(); 162 163 // Go through the journal and use the timestamp to create a 164 // unique ID for each entry 165 sd_journal* journalTmp = nullptr; 166 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 167 if (ret < 0) 168 { 169 BMCWEB_LOG_ERROR("failed to open journal: {}", ret); 170 messages::internalError(asyncResp->res); 171 return; 172 } 173 174 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 175 journalTmp, sd_journal_close); 176 journalTmp = nullptr; 177 178 // Seek to the end 179 if (sd_journal_seek_tail(journal.get()) < 0) 180 { 181 messages::internalError(asyncResp->res); 182 return; 183 } 184 185 // Get the last entry 186 if (sd_journal_previous(journal.get()) < 0) 187 { 188 messages::internalError(asyncResp->res); 189 return; 190 } 191 192 // Get the last sequence number 193 uint64_t endSeqNum = 0; 194 #if LIBSYSTEMD_VERSION >= 254 195 { 196 if (sd_journal_get_seqnum(journal.get(), &endSeqNum, nullptr) < 0) 197 { 198 messages::internalError(asyncResp->res); 199 return; 200 } 201 } 202 #endif 203 204 // Seek to the beginning 205 if (sd_journal_seek_head(journal.get()) < 0) 206 { 207 messages::internalError(asyncResp->res); 208 return; 209 } 210 211 // Get the first entry 212 if (sd_journal_next(journal.get()) < 0) 213 { 214 messages::internalError(asyncResp->res); 215 return; 216 } 217 218 // Get the first sequence number 219 uint64_t startSeqNum = 0; 220 #if LIBSYSTEMD_VERSION >= 254 221 { 222 if (sd_journal_get_seqnum(journal.get(), &startSeqNum, nullptr) < 0) 223 { 224 messages::internalError(asyncResp->res); 225 return; 226 } 227 } 228 #endif 229 230 BMCWEB_LOG_DEBUG("journal Sequence IDs start:{} end:{}", startSeqNum, 231 endSeqNum); 232 233 // Add 1 to account for the last entry 234 uint64_t totalEntries = endSeqNum - startSeqNum + 1; 235 asyncResp->res.jsonValue["Members@odata.count"] = totalEntries; 236 if (skip + top < totalEntries) 237 { 238 asyncResp->res.jsonValue["Members@odata.nextLink"] = 239 boost::urls::format( 240 "/redfish/v1/Managers/{}/LogServices/Journal/Entries?$skip={}", 241 BMCWEB_REDFISH_MANAGER_URI_NAME, std::to_string(skip + top)); 242 } 243 uint64_t index = 0; 244 if (skip > 0) 245 { 246 if (sd_journal_next_skip(journal.get(), skip) < 0) 247 { 248 messages::internalError(asyncResp->res); 249 return; 250 } 251 } 252 BMCWEB_LOG_DEBUG("Index was {}", index); 253 readJournalEntries(top, asyncResp, {std::move(journal)}); 254 } 255 256 inline void handleManagersJournalEntriesLogEntryGet( 257 App& app, const crow::Request& req, 258 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 259 const std::string& managerId, const std::string& entryID) 260 { 261 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 262 { 263 return; 264 } 265 266 if (managerId != BMCWEB_REDFISH_MANAGER_URI_NAME) 267 { 268 messages::resourceNotFound(asyncResp->res, "Manager", managerId); 269 return; 270 } 271 272 sd_journal* journalTmp = nullptr; 273 int ret = sd_journal_open(&journalTmp, SD_JOURNAL_LOCAL_ONLY); 274 if (ret < 0) 275 { 276 BMCWEB_LOG_ERROR("failed to open journal: {}", ret); 277 messages::internalError(asyncResp->res); 278 return; 279 } 280 std::unique_ptr<sd_journal, decltype(&sd_journal_close)> journal( 281 journalTmp, sd_journal_close); 282 journalTmp = nullptr; 283 284 std::string cursor; 285 if (!crow::utility::base64Decode(entryID, cursor)) 286 { 287 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 288 return; 289 } 290 291 // Go to the cursor in the log 292 ret = sd_journal_seek_cursor(journal.get(), cursor.c_str()); 293 if (ret < 0) 294 { 295 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 296 return; 297 } 298 299 if (sd_journal_next(journal.get()) < 0) 300 { 301 messages::internalError(asyncResp->res); 302 return; 303 } 304 305 ret = sd_journal_test_cursor(journal.get(), cursor.c_str()); 306 if (ret == 0) 307 { 308 messages::resourceNotFound(asyncResp->res, "LogEntry", entryID); 309 return; 310 } 311 if (ret < 0) 312 { 313 messages::internalError(asyncResp->res); 314 return; 315 } 316 317 nlohmann::json::object_t bmcJournalLogEntry; 318 if (!fillBMCJournalLogEntryJson(journal.get(), bmcJournalLogEntry)) 319 { 320 messages::internalError(asyncResp->res); 321 return; 322 } 323 asyncResp->res.jsonValue.update(bmcJournalLogEntry); 324 } 325 326 inline void requestRoutesBMCJournalLogService(App& app) 327 { 328 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/") 329 .privileges(redfish::privileges::getLogService) 330 .methods(boost::beast::http::verb::get)( 331 std::bind_front(handleManagersLogServiceJournalGet, std::ref(app))); 332 333 BMCWEB_ROUTE(app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/") 334 .privileges(redfish::privileges::getLogEntryCollection) 335 .methods(boost::beast::http::verb::get)(std::bind_front( 336 handleManagersJournalLogEntryCollectionGet, std::ref(app))); 337 338 BMCWEB_ROUTE( 339 app, "/redfish/v1/Managers/<str>/LogServices/Journal/Entries/<str>/") 340 .privileges(redfish::privileges::getLogEntry) 341 .methods(boost::beast::http::verb::get)(std::bind_front( 342 handleManagersJournalEntriesLogEntryGet, std::ref(app))); 343 } 344 } // namespace redfish 345