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