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